diff --git a/Modules/.build/Clear-BadPesterUserAccounts.ps1 b/Modules/.build/Clear-BadPesterUserAccounts.ps1 new file mode 100644 index 0000000..043d2f0 --- /dev/null +++ b/Modules/.build/Clear-BadPesterUserAccounts.ps1 @@ -0,0 +1,183 @@ +Function Clear-BadPesterUserAccounts { +<# +.SYNOPSIS + Cleans up all incorrectly created user accounts on the system from Pester. + +.EXAMPLE + Clear-BadPesterUserAccounts + +.OUTPUTS + Returns the list of folder names (from C:\Users) that are "invalid". + If the return list is empty then there were none to remove. + This is useful for post-facto additional resource cleanup. In the case of a non-WhatIf, this will be an ephemeral response, so catch it while you can. + +.PARAMETER AllPossibleDomainsToSearch + What domains do we want to search? Defaults to [fh, corp] + +.PARAMETER WhatIf + Used to test before removing for validation +#> + param( + $AllPossibleDomainsToSearch = @("fh","corp"), + [switch]$WhatIf + ) + + $logLead = (Get-LogLeadName) + + # This means we are on Windows + # At the very least, we can't get the CIM Instances to remove users if they exist + if ($null -eq (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)) { + Write-Host "$logLead : Can't CimInstance on this host, nothing to do" + return + } + + Write-Host "$logLead : Cleaning bad pester user accounts [Dry run? $WhatIf]" + + # Get local user accounts + # Get IIS user accounts or IIS app pool names (it defaults the identity to the name if no identity specified) + # Get all domain GMSA accounts + # Get all domain user accounts + # + # Get all c:\users folders + # Diff the names + # Remove the users + + # Annoying bug... + if ($null -ne (Get-Module Carbon)) { + # Carbon stomps all over our Get-ADDomainController amongst other things + Remove-Module Carbon -Force + } + + $userAccountsToExclude = @() + $foldersToKeep = @() + $foldersToRemove = @() + + # The system can't determine about some of these, they are special folders + # This is where any such folders go + # Try to make them dynamic before adding them here. + # Public is a very special child unrelated to anything else. + $userAccountsToExclude += "Public" + + if ($null -ne (Get-Module -ListAvailable -Name WebAdministration)) { + Get-Module WebAdministration | Remove-Module -Force + Import-Module WebAdministration + $appPools = @(Get-ChildItem IIS:\AppPools) + foreach($appPool in $appPools) { + if (![string]::IsNullOrWhiteSpace($appPool.processModel.userName)) { + $userAccountsToExclude += $appPool.processModel.userName + } else { + $userAccountsToExclude += $appPool.Name + } + } + } + + $localUsers = @(Get-LocalUser) + foreach($localUser in $localUsers) { + $userAccountsToExclude += $localUser.Name + } + + if ($null -eq (Get-Module -ListAvailable -Name ActiveDirectory)) { + # This is truly an impossible condition to hit. So if we hit it, hit it hard. + throw 'how in the name of sanity is this server not setup to query the domain???' + } + + Import-Module ActiveDirectory + foreach($domainName in $AllPossibleDomainsToSearch) { + $dc = Get-ADDomainController -DomainName $domainName -Discover -NextClosestSite + + $dcHostname = $dc.Hostname[0] + + if ([string]::IsNullOrWhiteSpace($dcHostname)) { + Write-Warning "$logLead : Could not find a hostname for $domainName - Can you access that domain from here?" + continue + } + + $domainServiceAccounts = @(Get-ADServiceAccount -Filter "*" -Server $dcHostname) + foreach($domainServiceAccount in $domainServiceAccounts) { + $userAccountsToExclude += $domainServiceAccount.SamAccountName + } + + $domainUserAccounts = @(Get-ADUser -Filter "*" -Server $dcHostname) + foreach($domainUserAccount in $domainUserAccounts) { + $userAccountsToExclude += $domainUserAccount.SamAccountName + } + } + + $userFolders = @(Get-ChildItem -Directory -Path C:\Users) + foreach($folder in $userFolders) { + if ($userAccountsToExclude -contains $folder.Name) { + $foldersToKeep += $folder.Name + } else { + $foldersToRemove += $folder.Name + } + } + + $allCimAccounts = @(Get-CimInstance -ClassName Win32_UserProfile) + + $teamCity = (Test-IsTeamCityProcess) + + if ($teamCity) { + Write-Host "##teamcity[blockOpened name='Display accounts for followup']" + } else { + Write-Host "=================================" + } + + if (!(Test-IsCollectionNullOrEmpty $foldersToKeep)) { + $tcBlurb = "Keep these accounts" + if ($teamCity) { + Write-Host "##teamcity[blockOpened name='$tcBlurb']" + } else { + Write-Host "$logLead : Found these folders to KEEP" + } + + foreach($folder in $foldersToKeep) { + if ($teamCity) { + Write-Host "$logLead : KEEP C:\Users\$folder" + } else { + Write-Verbose "$logLead : KEEP C:\Users\$folder" + } + } + + if ($teamCity) { + Write-Host "##teamcity[blockClosed name='$tcBlurb']" + } else { + Write-Host "=================================" + } + } + + if (!(Test-IsCollectionNullOrEmpty $foldersToRemove)) { + $tcBlurb = "Remove these accounts" + if ($teamCity) { + Write-Host "##teamcity[blockOpened name='$tcBlurb']" + } else { + Write-Host "$logLead : Found these folders to REMOVE" + } + + foreach($folder in $foldersToRemove) { + Write-Host "$logLead : REMOVE C:\Users\$folder" + $matchingCimInstances = @($allCimAccounts.Where({$_.LocalPath -eq "C:\Users\$folder"})) + + foreach($instance in $matchingCimInstances) { + if ($instance.Sid.StartsWith("S-1-5-82")) { + Write-Host "$logLead : $folder is an IIS-AppPool SID" + } + } + + $matchingCimInstances | Remove-CimInstance -WhatIf:$WhatIf + } + + if ($teamCity) { + Write-Host "##teamcity[blockClosed name='$tcBlurb']" + } else { + Write-Host "" + } + } + + if ($teamCity) { + Write-Host "##teamcity[blockClosed name='Display accounts for followup']" + } else { + Write-Host "=================================" + } + + return $foldersToRemove +} \ No newline at end of file diff --git a/Modules/.build/Get-Aliases.ps1 b/Modules/.build/Get-Aliases.ps1 new file mode 100644 index 0000000..82fc201 --- /dev/null +++ b/Modules/.build/Get-Aliases.ps1 @@ -0,0 +1,42 @@ +Function Get-Aliases { +<# +.SYNOPSIS + Collects all alias names from files in a folder. Assumes Test-FunctionNames properly ran and validated target function names. + +.EXAMPLE + Get-Aliases .\Alkami.PowerShell.IIS\Public + +.PARAMETER FolderPath + The name of the folder to examine all files under. +#> + [CmdletBinding()] + Param ( + [String]$FolderPath + ) + process { + $aliases = @() + $verbs = Get-Verb | Select-Object -ExpandProperty Verb + + $files = (Get-ChildItem -Path $FolderPath *.ps1) + foreach($file in $files) { + if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) { + Write-Verbose "skipping function names of test $file" + } else { + $lines = (Get-Content $file.FullName) + foreach($line in $lines) { + if (($line.Trim().ToLower().StartsWith("set-alias")) -or ($line.Trim().ToLower().StartsWith("new-alias"))) { + $candidateLine = $line -replace ';','' -replace '-name','' -replace '-value','' -replace '-force','' -replace '-scope:global','' -replace '-Scope Global','' + $splits = ($candidateLine.Trim() -split '\s+') + if ($splits.length -ne 3) { + Write-Warning "Could not parse line [$line] for Set-Alias, found [$splits]" + } else { + $aliases += $splits[1] + } + } + } + } + } + + return $aliases + } +} \ No newline at end of file diff --git a/Modules/.build/Get-BuildConfigs.ps1 b/Modules/.build/Get-BuildConfigs.ps1 new file mode 100644 index 0000000..a7d1630 --- /dev/null +++ b/Modules/.build/Get-BuildConfigs.ps1 @@ -0,0 +1,32 @@ +Function Get-BuildConfigs { +<# +.SYNOPSIS + Get the build configs from a single csproj +#> + [CmdletBinding()] + Param( + $csprojPath + ) + process { + if (!(Test-Path $csprojPath)) { + Write-Warning "can't test on an empty path [$csprojPath]" + return + } + + $returnValues = @() + + $xml = [Xml](Get-Content $csprojPath) + + $propertyGroups = @($xml.Project.PropertyGroup) + foreach($propertyGroup in $propertyGroups) { + if ($null -ne $propertyGroup.Condition) { + $value = $propertyGroup.Condition.Replace("'",'').Split('==')[-1].Split('|')[0].Trim() + if (!([string]::IsNullOrWhiteSpace($value))) { + $returnValues += $value + } + } + } + + return $returnValues + } +} diff --git a/Modules/.build/Get-ContentFromFilesInPath.ps1 b/Modules/.build/Get-ContentFromFilesInPath.ps1 new file mode 100644 index 0000000..e90dc36 --- /dev/null +++ b/Modules/.build/Get-ContentFromFilesInPath.ps1 @@ -0,0 +1,58 @@ +Function Get-ContentFromFilesInPath { +<# +.SYNOPSIS + Collects all content from valid files for inclusion into the PSM1 + +.DESCRIPTION + Collects all content from valid files for inclusion into the PSM1 + * Skips test files + * Adds a header line saying where the file came from during compile + The reason for these files to match their names is about process, not a functional concern. + +.EXAMPLE + Get-ContentFromFilesInPath .\Alkami.PowerShell.IIS\ + +.PARAMETER FolderPath + The name of the folder to examine all files under. +#> + [CmdletBinding()] + Param ( + [String]$FolderPath + ) + process { + $functionLines = @() + + $prefixPath = (Split-Path $FolderPath -leaf) + + $files = (Get-ChildItem (Join-Path $FolderPath "*.ps1")) + + foreach($file in $files) { + if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) { + Write-Verbose "skipping function names of test $file" + } + elseif ($file.BaseName.ToLower() -eq "variabledeclarations") { + + $content = Get-Content $file.FullName + $functionLines += @("## Function from $file") + + if ($null -ne $content -and $content[0] -match "SuppressMessageAttribute" -and $content[1] -match "param\(\)") { + + Write-Host "Skipping PSScriptAnalyzer and Param Declaration from $($file.FullName)" -ForegroundColor Green + $functionLines += ($content | Select-Object -Skip 2) + } else { + + $functionLines += $content + } + } + else { + $functionLines += @("## Function from $file") + + $functionLines += (Get-Content $file.FullName) + } + + $functionLines += "" + } + + return $functionLines + } +} \ No newline at end of file diff --git a/Modules/.build/Get-ContentFromFormatFilesInPath.ps1 b/Modules/.build/Get-ContentFromFormatFilesInPath.ps1 new file mode 100644 index 0000000..a04020d --- /dev/null +++ b/Modules/.build/Get-ContentFromFormatFilesInPath.ps1 @@ -0,0 +1,57 @@ +Function Get-ContentFromFormatFilesInPath { +<# +.SYNOPSIS + Collects all content from valid files for inclusion into the PSM1 + +.DESCRIPTION + Collects all content from valid files for inclusion into the PSM1 + * Skips test files + * Adds a header line saying where the file came from during compile + The reason for these files to match their names is about process, not a functional concern. + +.EXAMPLE + Get-ContentFromFilesInPath .\Alkami.PowerShell.IIS\ + +.PARAMETER FolderPath + The name of the folder to examine all files under. +#> + [CmdletBinding()] + Param ( + [String]$FolderPath + ) + process { + $functionLines = @() + + $prefixPath = (Split-Path $FolderPath -leaf) + + $files = (Get-ChildItem (Join-Path $FolderPath "*.ps1xml")) + + [Xml]$defaultXml = [Xml]@" + + + +"@ + $defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("SelectionSets")) | Out-Null + $defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("ViewDefinitions")) | Out-Null + $defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("Controls")) | Out-Null + $defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("DefaultSettings")) | Out-Null + + foreach($file in $files) { + [Xml]$xml = [Xml](Get-Content $file.FullName) + foreach($node in $xml.DocumentElement.SelectionSets.ChildNodes) { + $defaultXml.DocumentElement.SelectSingleNode("SelectionSets").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null + } + foreach($node in $xml.DocumentElement.Controls.ChildNodes) { + $defaultXml.DocumentElement.SelectSingleNode("Controls").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null + } + foreach($node in $xml.DocumentElement.ViewDefinitions.ChildNodes) { + $defaultXml.DocumentElement.SelectSingleNode("ViewDefinitions").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null + } + foreach($node in $xml.DocumentElement.DefaultSettings.ChildNodes) { + $defaultXml.DocumentElement.SelectSingleNode("DefaultSettings").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null + } + } + + return $defaultXml + } +} \ No newline at end of file diff --git a/Modules/.build/Get-FunctionNames.ps1 b/Modules/.build/Get-FunctionNames.ps1 new file mode 100644 index 0000000..3a9f452 --- /dev/null +++ b/Modules/.build/Get-FunctionNames.ps1 @@ -0,0 +1,31 @@ +Function Get-FunctionNames { +<# +.SYNOPSIS + Collects all function names from filenames. Assumes Test-FunctionNames properly ran. + +.EXAMPLE + Get-FunctionNames .\Alkami.PowerShell.IIS\Public + +.PARAMETER FolderPath + The name of the folder to examine all files under. +#> + [CmdletBinding()] + Param ( + [String]$FolderPath + ) + process { + $functionNames = @() + $verbs = Get-Verb | Select-Object -ExpandProperty Verb + + $files = (Get-ChildItem -Path $FolderPath *.ps1) + foreach($file in $files) { + if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) { + Write-Verbose "skipping function names of test $file" + } else { + $functionNames += $file.BaseName + } + } + + return $functionNames + } +} \ No newline at end of file diff --git a/Modules/.build/Get-MSBuildPath.ps1 b/Modules/.build/Get-MSBuildPath.ps1 new file mode 100644 index 0000000..1f033f5 --- /dev/null +++ b/Modules/.build/Get-MSBuildPath.ps1 @@ -0,0 +1,41 @@ +Function Get-MSBuildPath { + <# +.SYNOPSIS + Get the local machine MSBuild path in a way that it can be easily consumed + +.EXAMPLE + Get-MSBuildPath + +C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe +#> + [CmdletBinding()] + Param() + process { + if ($script:isSet_GetMSBuildPath) { + return $script:value_GetMSBuildPath + } + $msBuildPath = "" + if (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe") { + $msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe" + } elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe") { + $msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe" + } elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe") { + $msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe" + } elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe") { + $msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe" + } elseif (Test-Path "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe") { + $msBuildPath = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe" + } else { + $buildVersion = (Get-ChildItem HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions -Name | Measure -Maximum).Maximum + $MSBuildVersionRegEntry = (Get-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$($buildVersion).0\" -Name "msbuild.exe") + if ($null -ne $MSBuildVersionRegEntry) { + $msBuildPath = $MSBuildVersionRegEntry."msbuild.exe" + } + } + if (!([string]::IsNullOrEmpty($msBuildPath))) { + $script:isSet_GetMSBuildPath = $true + $script:value_GetMSBuildPath = $msBuildPath + } + return $msBuildPath + } +} diff --git a/Modules/.build/Join-PS1XMLFromFiles.ps1 b/Modules/.build/Join-PS1XMLFromFiles.ps1 new file mode 100644 index 0000000..09794c1 --- /dev/null +++ b/Modules/.build/Join-PS1XMLFromFiles.ps1 @@ -0,0 +1,29 @@ +Function Join-PS1XMLFromFiles { +<# +.SYNOPSIS + Uses Get-ContentFromFormatFilesInPath to join the file contents appropriately + +.DESCRIPTION + Collects all content from valid files for inclusion into the PS1XML file + +.EXAMPLE + Join-PS1XMLFromFiles .\Alkami.PowerShell.IIS\Public\ .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.ps1xml + +.PARAMETER PublicPath + The name of the folder to examine all files under. + +.PARAMETER Ps1xmlFilePath + The file to overwrite with the contents of the build process. +#> + [CmdletBinding()] + Param ( + [String]$PublicPath, + $Ps1xmlFilePath + ) + process { + # Get all of the xml nodes from the format files + $nodes = @(Get-ContentFromFormatFilesInPath $PublicPath) + + Set-Content -Path $Ps1xmlFilePath -Value $nodes.OuterXml -Force + } +} \ No newline at end of file diff --git a/Modules/.build/Join-PSM1FromFiles.ps1 b/Modules/.build/Join-PSM1FromFiles.ps1 new file mode 100644 index 0000000..f0f87b2 --- /dev/null +++ b/Modules/.build/Join-PSM1FromFiles.ps1 @@ -0,0 +1,41 @@ +Function Join-PSM1FromFiles { +<# +.SYNOPSIS + Uses Get-ContentFromFilesInPath to join the file contents appropriately + +.DESCRIPTION + Collects all content from valid files for inclusion into the PSM1 + * Skips test files + * Adds a header line saying where the file came from during compile + The reason for these files to match their names is about process, not a functional concern. + +.EXAMPLE + Join-PSM1FromFiles .\Alkami.PowerShell.IIS\Public\ .\Alkami.PowerShell.IIS\Private\ .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psm1 + +.PARAMETER PublicPath + The name of the folder to examine all files under. + +.PARAMETER PrivatePath + The name of the folder to examine all files under. Tested for existence, may not be valid. + +.PARAMETER Psm1FilePath + The file to overwrite with the contents of the build process. +#> + [CmdletBinding()] + Param ( + [String]$PublicPath, + [String]$PrivatePath, + $Psm1FilePath + ) + process { + ## Put all of the content from the public files in a long array with a file-header + $functionLines = @(Get-ContentFromFilesInPath $PublicPath) + + if (Test-Path $PrivatePath) { + ## Put all of the content from the private files in the array with a file-header, after the public functions + $functionLines += @(Get-ContentFromFilesInPath $PrivatePath) + } + + Set-Content -Path $Psm1FilePath -Value $functionLines -Force + } +} \ No newline at end of file diff --git a/Modules/.build/Load-Includes.ps1 b/Modules/.build/Load-Includes.ps1 new file mode 100644 index 0000000..f94e0d4 --- /dev/null +++ b/Modules/.build/Load-Includes.ps1 @@ -0,0 +1,24 @@ +## This file tries to load functions if they haven't been loaded into scope yet. +## We know if things are in the scope if the magic function Get-ContentFromFilesInPath has been loaded into the current session scope +## The way that this is loaded is from this file or manually +## This file was chosen because it doesn't live in any module for building the powershell modules + +## The use of this file is as such: +## . $PSScriptRoot\.build\Load-Includes.ps1 +## The use-case of this file and the unusual style is that this is just to include things for execution in the build system +## This is not a typical style of how to load things. This is a micro-op. +$script:buildFilesLoaded = $script:buildFilesLoaded -or ($null -ne ${function:Get-ContentFromFilesInPath}) + +if (!($script:buildFilesLoaded)) { + $buildScriptsFolder = $PSScriptRoot + + if (Test-Path $buildScriptsFolder) { + $buildScripts = (Get-ChildItem *.ps1 -Path $buildScriptsFolder -Exclude "Load-Includes.ps1" -Recurse) + foreach($script in $buildScripts) { + Write-Verbose "[Clean-Project] - Including the build files: $script" + . $script.FullName + } + + $script:buildFilesLoaded = $true + } +} \ No newline at end of file diff --git a/Modules/.build/Test-FunctionNames.ps1 b/Modules/.build/Test-FunctionNames.ps1 new file mode 100644 index 0000000..9486526 --- /dev/null +++ b/Modules/.build/Test-FunctionNames.ps1 @@ -0,0 +1,86 @@ +Function Test-FunctionNames { +<# +.SYNOPSIS + Test that the name of the function/filter/workflow in the file match the name of the file and for verb match + +.DESCRIPTION + Test that the name of the function/filter/workflow in the file match the name of the file and for verb match + * This will look in public/private folders + * This will examine for verb match to Get-Verb as part of the testing + * This ignores any names that end with .tests.ps1 or .test.ps1 + * This will check .tests?.ps1 files that they start with a reference to Load-PesterModules + * If they don't, it will emit a warning per file + The reason for these files to match their names is about process, not a functional concern. + +.EXAMPLE + Test-FunctionNames .\Alkami.PowerShell.IIS\ + +.PARAMETER FolderPath + The name of the folder to examine all files under +#> + [CmdletBinding()] + Param ( + [String]$FolderPath + ) + process { + $verbs = Get-Verb | Select-Object -ExpandProperty Verb + + $badMatch = @() + $files = (Get-ChildItem (Join-Path $FolderPath "*.ps1")) + foreach($file in $files) { + if ($file.FullName -match '(scratch|bak)') { + Write-Verbose "Skipping $($file.FullName) for (scratch|bak)" + continue + } + $content = (Get-Content $file) + if ($file -match '\.Tests\.ps1' -or $file -match '\.Test\.ps1') { + $firstLine = $content[0] + if (([string]::IsNullOrWhitespace($firstLine) -or $firstLine -notmatch 'PesterModules') -and $firstline -notmatch "param") { + Write-Warning "[$file] does not start with load-pestermodules or parameter declarations. You're gonna have a bad time." + } elseif ($firstLine -match "param" -and ($null -eq ($content | Where-Object {$_ -match "PesterModules"}))) { + Write-Warning "[$file] has a parameter declaration but does not include load-pestermodules. You're gonna have a bad time." + } + + Write-Verbose "skipping testing of test $file" + } else { + $functionCandidate = $file.BaseName + $keywordStartsLine = 0 + + $passing = $false + foreach($line in $content) { + ## Make sure the line doesn't start with whitespace and that people didn't put a bunch of spaces between 'keyword' and 'function-name' + $line = ($line -replace '\s+',' ' -replace '^function','' -replace '^filter','' -replace '^workflow','').Trim() + + if ($line.StartsWith($functionCandidate, [StringComparison]::InvariantCultureIgnoreCase)) { + $passing = $true + ## we found the line we want + break + } + + $keywordStartsLine += 1 + } + + # Make sure that the function name uses a good verb-naming pattern + $verb = $functionCandidate.Split("-")[0] + + if($verbs -notcontains $verb) { + $passing = $false + Write-Warning "Function $functionCandidate (current verb: [$verb]) does not use a valid PowerShell verb. See: Get-Verb" + } + + if ($keywordStartsLine -gt 1) { + Write-Verbose "The file [$file] doesn't start with the keyword and function-name" + } + + if (!$passing) { + $badMatch += $functionCandidate + } + } + } + + if ($badMatch.Length -gt 0) { + $badMatch + throw "The previous files failed their test constraints" + } + } +} \ No newline at end of file diff --git a/Modules/.build/Test-IsTeamCityProcess.ps1 b/Modules/.build/Test-IsTeamCityProcess.ps1 new file mode 100644 index 0000000..0b18a17 --- /dev/null +++ b/Modules/.build/Test-IsTeamCityProcess.ps1 @@ -0,0 +1,23 @@ +Function Test-IsTeamCityProcess { +<# +.SYNOPSIS + Determines if the Current Process is a TeamCity Agent Process + +.NOTES + Will not work except when running in a direct agent process. Checks for TeamCity agent environment + variables to exist. +#> +## Duplicates some of Alkami.DevOps.Common\Public\Test-IsTeamCityProcess.ps1 + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + $teamCityJREVariable = $ENV:TEAMCITY_JRE + + if ([String]::IsNullOrEmpty($teamCityJREVariable)) { + Write-Verbose "[Test-IsTeamCityProcess] TEAMCITY_JRE User Environment Variable Not Found" + return $false + } + + return $true +} \ No newline at end of file diff --git a/Modules/.build/Test-ModuleInclusion.ps1 b/Modules/.build/Test-ModuleInclusion.ps1 new file mode 100644 index 0000000..dc25730 --- /dev/null +++ b/Modules/.build/Test-ModuleInclusion.ps1 @@ -0,0 +1,473 @@ +Function Test-ModuleInclusion { +<# +.SYNOPSIS + Checks all the modules in the solution to see if there are obvious circular dependencies or if the module includes a higher-order include + +.EXAMPLE + Test-ModuleInclusion .\ + +.PARAMETER FolderPath + The name of the folder to examine all files under. + +.PARAMETER FailBuild + Throws if this is true + +.PARAMETER ShowObviousCycles + Show obvious cycles. This is not as important as higher-order inclusion on lower-order modules +#> + [CmdletBinding()] + Param ( + [string]$FolderPath, + [switch]$FailBuild, + [switch]$ShowObviousCycles + ) + +$cSharpCode = @" +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Collections; + + + public static class ModuleChecker { + #region static decls + static string[] verbs = new[] { "Add", "Approve", "Assert", "Backup", "Block", "Checkpoint", "Clear", "Close", "Compare", "Complete", "Compress", "Confirm", "Connect", "Convert", "ConvertFrom", "ConvertTo", "Copy", "Debug", "Deny", "Disable", "Disconnect", "Dismount", "Edit", "Enable", "Enter", "Exit", "Expand", "Export", "Find", "Format", "Get", "Grant", "Group", "Hide", "Import", "Initialize", "Install", "Invoke", "Join", "Limit", "Lock", "Measure", "Merge", "Mount", "Move", "New", "Open", "Optimize", "Out", "Ping", "Pop", "Protect", "Publish", "Push", "Read", "Receive", "Redo", "Register", "Remove", "Rename", "Repair", "Request", "Reset", "Resize", "Resolve", "Restart", "Restore", "Resume", "Revoke", "Save", "Search", "Select", "Send", "Set", "Show", "Skip", "Split", "Start", "Step", "Stop", "Submit", "Suspend", "Switch", "Sync", "Test", "Trace", "Unblock", "Undo", "Uninstall", "Unlock", "Unprotect", "Unpublish", "Unregister", "Update", "Use", "Wait", "Watch", "Write" }; + + static string[] webAdministrationFunctions = new[] { "Add-WebConfiguration", "Add-WebConfigurationLock", "Add-WebConfigurationProperty", "Backup-WebConfiguration", "Clear-WebCentralCertProvider", "Clear-WebConfiguration", "Clear-WebRequestTracingSetting", "Clear-WebRequestTracingSettings", "ConvertTo-WebApplication", "Disable-WebCentralCertProvider", "Disable-WebGlobalModule", "Disable-WebRequestTracing", "Enable-WebCentralCertProvider", "Enable-WebGlobalModule", "Enable-WebRequestTracing", "Get-WebAppDomain", "Get-WebApplication", "Get-WebAppPoolState", "Get-WebBinding", "Get-WebCentralCertProvider", "Get-WebConfigFile", "Get-WebConfiguration", "Get-WebConfigurationBackup", "Get-WebConfigurationLocation", "Get-WebConfigurationLock", "Get-WebConfigurationProperty", "Get-WebFilePath", "Get-WebGlobalModule", "Get-WebHandler", "Get-WebItemState", "Get-WebManagedModule", "Get-WebRequest", "Get-Website", "Get-WebsiteState", "Get-WebURL", "Get-WebVirtualDirectory", "New-WebApplication", "New-WebAppPool", "New-WebBinding", "New-WebFtpSite", "New-WebGlobalModule", "New-WebHandler", "New-WebManagedModule", "New-Website", "New-WebVirtualDirectory", "Remove-WebApplication", "Remove-WebAppPool", "Remove-WebBinding", "Remove-WebConfigurationBackup", "Remove-WebConfigurationLocation", "Remove-WebConfigurationLock", "Remove-WebConfigurationProperty", "Remove-WebGlobalModule", "Remove-WebHandler", "Remove-WebManagedModule", "Remove-Website", "Remove-WebVirtualDirectory", "Rename-WebConfigurationLocation", "Restart-WebAppPool", "Restart-WebItem", "Restore-WebConfiguration", "Select-WebConfiguration", "Set-WebBinding", "Set-WebCentralCertProvider", "Set-WebCentralCertProviderCredential", "Set-WebConfiguration", "Set-WebConfigurationProperty", "Set-WebGlobalModule", "Set-WebHandler", "Set-WebManagedModule", "Start-WebAppPool", "Start-WebCommitDelay", "Start-WebItem", "Start-Website", "Stop-WebAppPool", "Stop-WebCommitDelay", "Stop-WebItem", "Stop-Website" }; + + static string[] carbonFunctions = new[] { "Add-CGroupMember", "Add-CIisDefaultDocument", "Add-CTrustedHost", "Add-GroupMember", "Add-GroupMembers", "Add-IisDefaultDocument", "Add-TrustedHost", "Add-TrustedHosts", "Assert-AdminPrivilege", "Assert-AdminPrivileges", "Assert-CAdminPrivilege", "Assert-CFirewallConfigurable", "Assert-CService", "Assert-FirewallConfigurable", "Assert-Service", "Clear-CDscLocalResourceCache", "Clear-CMofAuthoringMetadata", "Clear-CTrustedHost", "Clear-DscLocalResourceCache", "Clear-MofAuthoringMetadata", "Clear-TrustedHost", "Clear-TrustedHosts", "Complete-CJob", "Complete-Job", "Complete-Jobs", "Compress-CItem", "Compress-Item", "Convert-CSecureStringToString", "Convert-CXmlFile", "Convert-SecureStringToString", "Convert-XmlFile", "ConvertFrom-Base64", "ConvertFrom-CBase64", "ConvertTo-Base64", "ConvertTo-CBase64", "ConvertTo-CContainerInheritanceFlags", "ConvertTo-CInheritanceFlag", "ConvertTo-CPropagationFlag", "ConvertTo-CSecurityIdentifier", "ConvertTo-ContainerInheritanceFlags", "ConvertTo-FullPath", "ConvertTo-InheritanceFlag", "ConvertTo-InheritanceFlags", "ConvertTo-PropagationFlag", "ConvertTo-PropagationFlags", "ConvertTo-SecurityIdentifier", "Copy-CDscResource", "Copy-DscResource", "Disable-AclInheritance", "Disable-CAclInheritance", "Disable-CFirewallStatefulFtp", "Disable-CIEEnhancedSecurityConfiguration", "Disable-CIisSecurityAuthentication", "Disable-CNtfsCompression", "Disable-FirewallStatefulFtp", "Disable-IEEnhancedSecurityConfiguration", "Disable-IisSecurityAuthentication", "Disable-NtfsCompression", "Enable-AclInheritance", "Enable-CAclInheritance", "Enable-CFirewallStatefulFtp", "Enable-CIEActivationPermission", "Enable-CIisDirectoryBrowsing", "Enable-CIisSecurityAuthentication", "Enable-CIisSsl", "Enable-CNtfsCompression", "Enable-FirewallStatefulFtp", "Enable-IEActivationPermission", "Enable-IEActivationPermissions", "Enable-IisDirectoryBrowsing", "Enable-IisSecurityAuthentication", "Enable-IisSsl", "Enable-NtfsCompression", "Expand-CItem", "Expand-Item", "Find-ADUser", "Find-CADUser", "Format-ADSearchFilterValue", "Format-ADSpecialCharacters", "Format-CADSearchFilterValue", "Get-ADDomainController", "Get-CADDomainController", "Get-CCertificate", "Get-CCertificateStore", "Get-CComPermission", "Get-CComSecurityDescriptor", "Get-CDscError", "Get-CDscWinEvent", "Get-CFileShare", "Get-CFileSharePermission", "Get-CFirewallRule", "Get-CGroup", "Get-CHttpUrlAcl", "Get-CIPAddress", "Get-CIisAppPool", "Get-CIisApplication", "Get-CIisConfigurationSection", "Get-CIisHttpHeader", "Get-CIisHttpRedirect", "Get-CIisMimeMap", "Get-CIisSecurityAuthentication", "Get-CIisVersion", "Get-CIisWebsite", "Get-CMsi", "Get-CMsmqMessageQueue", "Get-CMsmqMessageQueuePath", "Get-CPathProvider", "Get-CPathToHostsFile", "Get-CPerformanceCounter", "Get-CPermission", "Get-CPowerShellModuleInstallPath", "Get-CPowershellPath", "Get-CPrivilege", "Get-CProgramInstallInfo", "Get-CRegistryKeyValue", "Get-CScheduledTask", "Get-CServiceAcl", "Get-CServiceConfiguration", "Get-CServicePermission", "Get-CServiceSecurityDescriptor", "Get-CSslCertificateBinding", "Get-CTrustedHost", "Get-CUser", "Get-CWmiLocalUserAccount", "Get-Certificate", "Get-CertificateStore", "Get-ComPermission", "Get-ComPermissions", "Get-ComSecurityDescriptor", "Get-DscError", "Get-DscWinEvent", "Get-FileShare", "Get-FileSharePermission", "Get-FirewallRule", "Get-FirewallRules", "Get-Group", "Get-HttpUrlAcl", "Get-IPAddress", "Get-IisAppPool", "Get-IisApplication", "Get-IisConfigurationSection", "Get-IisHttpHeader", "Get-IisHttpRedirect", "Get-IisMimeMap", "Get-IisSecurityAuthentication", "Get-IisVersion", "Get-IisWebsite", "Get-Msi", "Get-MsmqMessageQueue", "Get-MsmqMessageQueuePath", "Get-PathCanonicalCase", "Get-PathProvider", "Get-PathToHostsFile", "Get-PerformanceCounter", "Get-PerformanceCounters", "Get-Permission", "Get-Permissions", "Get-PowerShellModuleInstallPath", "Get-PowershellPath", "Get-Privilege", "Get-Privileges", "Get-ProgramInstallInfo", "Get-RegistryKeyValue", "Get-ScheduledTask", "Get-ServiceAcl", "Get-ServiceConfiguration", "Get-ServicePermission", "Get-ServicePermissions", "Get-ServiceSecurityDescriptor", "Get-SslCertificateBinding", "Get-SslCertificateBindings", "Get-TrustedHost", "Get-TrustedHosts", "Get-User", "Get-WmiLocalUserAccount", "Grant-CComPermission", "Grant-CHttpUrlPermission", "Grant-CMsmqMessageQueuePermission", "Grant-CPermission", "Grant-CPrivilege", "Grant-CServiceControlPermission", "Grant-CServicePermission", "Grant-ComPermission", "Grant-ComPermissions", "Grant-HttpUrlPermission", "Grant-MsmqMessageQueuePermission", "Grant-MsmqMessageQueuePermissions", "Grant-Permission", "Grant-Permissions", "Grant-Privilege", "Grant-ServiceControlPermission", "Grant-ServicePermission", "Initialize-CLcm", "Initialize-Lcm", "Install-CCertificate", "Install-CDirectory", "Install-CFileShare", "Install-CGroup", "Install-CIisAppPool", "Install-CIisApplication", "Install-CIisVirtualDirectory", "Install-CIisWebsite", "Install-CJunction", "Install-CMsi", "Install-CMsmq", "Install-CMsmqMessageQueue", "Install-CPerformanceCounter", "Install-CRegistryKey", "Install-CScheduledTask", "Install-CService", "Install-CUser", "Install-Certificate", "Install-Directory", "Install-FileShare", "Install-Group", "Install-IisAppPool", "Install-IisApplication", "Install-IisVirtualDirectory", "Install-IisWebsite", "Install-Junction", "Install-Msi", "Install-Msmq", "Install-MsmqMessageQueue", "Install-PerformanceCounter", "Install-RegistryKey", "Install-ScheduledTask", "Install-Service", "Install-SmbShare", "Install-User", "Invoke-AppCmd", "Invoke-CAppCmd", "Invoke-CPowerShell", "Invoke-PowerShell", "Invoke-WindowsInstaller", "Join-CIisVirtualPath", "Join-IisVirtualPath", "Lock-CIisConfigurationSection", "Lock-IisConfigurationSection", "New-CCredential", "New-CJunction", "New-CRsaKeyPair", "New-CTempDirectory", "New-Credential", "New-Junction", "New-RsaKeyPair", "New-TempDir", "New-TempDirectory", "Protect-Acl", "Protect-CString", "Protect-String", "Read-CFile", "Read-File", "Remove-CDotNetAppSetting", "Remove-CEnvironmentVariable", "Remove-CGroupMember", "Remove-CHostsEntry", "Remove-CIisMimeMap", "Remove-CIniEntry", "Remove-CJunction", "Remove-CRegistryKeyValue", "Remove-CSslCertificateBinding", "Remove-Certificate", "Remove-DotNetAppSetting", "Remove-EnvironmentVariable", "Remove-GroupMember", "Remove-HostsEntry", "Remove-IisMimeMap", "Remove-IisWebsite", "Remove-IniEntry", "Remove-Junction", "Remove-MsmqMessageQueue", "Remove-RegistryKeyValue", "Remove-Service", "Remove-SslCertificateBinding", "Remove-User", "Reset-CHostsFile", "Reset-CMsmqQueueManagerID", "Reset-HostsFile", "Reset-MsmqQueueManagerID", "Resolve-CFullPath", "Resolve-CIdentity", "Resolve-CIdentityName", "Resolve-CNetPath", "Resolve-CPathCase", "Resolve-CRelativePath", "Resolve-FullPath", "Resolve-Identity", "Resolve-IdentityName", "Resolve-NetPath", "Resolve-PathCase", "Resolve-RelativePath", "Restart-CRemoteService", "Restart-RemoteService", "Revoke-CComPermission", "Revoke-CHttpUrlPermission", "Revoke-CPermission", "Revoke-CPrivilege", "Revoke-CServicePermission", "Revoke-ComPermission", "Revoke-ComPermissions", "Revoke-HttpUrlPermission", "Revoke-Permission", "Revoke-Privilege", "Revoke-ServicePermission", "Set-CDotNetAppSetting", "Set-CDotNetConnectionString", "Set-CEnvironmentVariable", "Set-CHostsEntry", "Set-CIisHttpHeader", "Set-CIisHttpRedirect", "Set-CIisMimeMap", "Set-CIisWebsiteID", "Set-CIisWebsiteSslCertificate", "Set-CIisWindowsAuthentication", "Set-CIniEntry", "Set-CRegistryKeyValue", "Set-CServiceAcl", "Set-CSslCertificateBinding", "Set-CTrustedHost", "Set-DotNetAppSetting", "Set-DotNetConnectionString", "Set-EnvironmentVariable", "Set-HostsEntry", "Set-IisHttpHeader", "Set-IisHttpRedirect", "Set-IisMimeMap", "Set-IisWebsiteID", "Set-IisWebsiteSslCertificate", "Set-IisWindowsAuthentication", "Set-IniEntry", "Set-RegistryKeyValue", "Set-ServiceAcl", "Set-SslCertificateBinding", "Set-TrustedHost", "Set-TrustedHosts", "Split-CIni", "Split-Ini", "Start-CDscPullConfiguration", "Start-DscPullConfiguration", "Test-AdminPrivilege", "Test-AdminPrivileges", "Test-CAdminPrivilege", "Test-CDotNet", "Test-CDscTargetResource", "Test-CFileShare", "Test-CFirewallStatefulFtp", "Test-CGroup", "Test-CGroupMember", "Test-CIPAddress", "Test-CIdentity", "Test-CIisAppPool", "Test-CIisConfigurationSection", "Test-CIisSecurityAuthentication", "Test-CIisWebsite", "Test-CMsmqMessageQueue", "Test-CNtfsCompression", "Test-COSIs32Bit", "Test-COSIs64Bit", "Test-CPathIsJunction", "Test-CPerformanceCounter", "Test-CPerformanceCounterCategory", "Test-CPermission", "Test-CPowerShellIs32Bit", "Test-CPowerShellIs64Bit", "Test-CPrivilege", "Test-CRegistryKeyValue", "Test-CScheduledTask", "Test-CService", "Test-CSslCertificateBinding", "Test-CTypeDataMember", "Test-CUncPath", "Test-CUser", "Test-CWindowsFeature", "Test-CZipFile", "Test-DotNet", "Test-DscTargetResource", "Test-FileShare", "Test-FirewallStatefulFtp", "Test-Group", "Test-GroupMember", "Test-IPAddress", "Test-Identity", "Test-IisAppPool", "Test-IisAppPoolExists", "Test-IisConfigurationSection", "Test-IisSecurityAuthentication", "Test-IisWebsite", "Test-IisWebsiteExists", "Test-MsmqMessageQueue", "Test-NtfsCompression", "Test-OSIs32Bit", "Test-OSIs64Bit", "Test-PathIsJunction", "Test-PerformanceCounter", "Test-PerformanceCounterCategory", "Test-Permission", "Test-PowerShellIs32Bit", "Test-PowerShellIs64Bit", "Test-Privilege", "Test-RegistryKeyValue", "Test-ScheduledTask", "Test-Service", "Test-SslCertificateBinding", "Test-TypeDataMember", "Test-UncPath", "Test-User", "Test-WindowsFeature", "Test-ZipFile", "Uninstall-CCertificate", "Uninstall-CDirectory", "Uninstall-CFileShare", "Uninstall-CGroup", "Uninstall-CIisAppPool", "Uninstall-CIisWebsite", "Uninstall-CJunction", "Uninstall-CMsmqMessageQueue", "Uninstall-CPerformanceCounterCategory", "Uninstall-CScheduledTask", "Uninstall-CService", "Uninstall-CUser", "Uninstall-Certificate", "Uninstall-Directory", "Uninstall-FileShare", "Uninstall-Group", "Uninstall-IisAppPool", "Uninstall-IisWebsite", "Uninstall-Junction", "Uninstall-MsmqMessageQueue", "Uninstall-PerformanceCounterCategory", "Uninstall-ScheduledTask", "Uninstall-Service", "Uninstall-User", "Unlock-CIisConfigurationSection", "Unlock-IisConfigurationSection", "Unprotect-AclAccessRules", "Unprotect-CString", "Unprotect-String", "Write-CDscError", "Write-CFile", "Write-DscError", "Write-File" }; + + static string[] windowsFunctions = new[] { "Add-LocalGroupMember", "Add-Member", "Add-Type", "Add-WindowsFeature", "Clear-Host", "Close-SmbOpenFile", "Compare-Object", "Compress-Certificates", "Convert-Path", "ConvertFrom-Json", "ConvertTo-JSON", "ConvertTo-Json", "ConvertTo-SecureString", "ConvertTo-Xml", "Copy-Item", "Enable-WindowsOptionalFeature", "Enter-PSSession", "Exit-PSSession", "Export-PfxCertificate", "Format-List", "Format-Table", "Get-ADComputer", "Get-ADGroupMember", "Get-ADUser", "Get-Acl", "Get-CIMInstance", "Get-ChildItem", "Get-CimInstance", "Get-Content", "Get-Date", "Get-FileHash", "Get-Host", "Get-IISAppPool", "Get-IISSite", "Get-Item", "Get-ItemProperty", "Get-ItemPropertyValue", "Get-LocalGroupMember", "Get-Location", "Get-Member", "Get-Module", "Get-NetIPAddress", "Get-NetTCPConnection", "Get-PSCallStack", "Get-Process", "Get-Random", "Get-Service", "Get-SmbOpenFile", "Get-TimeZone", "Get-Unique", "Get-Variable", "Get-WinEvent", "Get-WindowsFeature", "Get-Childitem", "Group-Object", "Import-Certificate", "Import-Csv", "Import-Module", "Import-PfxCertificate", "Install-ADServiceAccount", "Install-WindowsFeature", "Invoke-Command", "Invoke-Expression", "Invoke-Pester", "Invoke-RestMethod", "Invoke-WebRequest", "Join-Path", "Join-path", "Move-Item", "New-EventLog", "New-Item", "New-ItemProperty", "New-Object", "New-PSDrive", "New-PSSession", "New-ScheduledTaskAction", "New-ScheduledTaskTrigger", "New-Service", "New-TimeSpan", "New-Variable", "New-WebServiceProxy", "Out-File", "Out-Null", "Out-String", "Out-null", "Pop-Location", "Push-Location", "Read-Host", "Receive-Job", "Register-ScheduledTask", "Remove-Item", "Remove-PSDrive", "Remove-PSSession", "Remove-item", "Rename-Item", "Resolve-DnsName", "Resolve-Path", "Select-Object", "Select-Xml", "Select-object", "Send-MailMessage", "Set-Acl", "Set-Alias", "Set-Content", "Set-ExecutionPolicy", "Set-ItemProperty", "Set-Location", "Set-Service", "Set-Variable", "Split-Path", "Start-Job", "Start-Process", "Start-Service", "Start-Sleep", "Stop-Process", "Stop-Service", "Test-ADServiceAccount", "Test-NetConnection", "Test-Path", "Wait-Job", "Write-Debug", "Write-Error", "Write-Host", "Write-Information", "Write-Output", "Write-Verbose", "Write-Warning" ,"Restart-Computer","Restart-Service","Invoke-Item","Get-Counter","ConvertTo-Html","New-Guid"}; + + static string[] awsFunctions = new[] { "Remove-S3Object", "Write-S3Object", "Get-S3Object", "Copy-S3Object", "Copy-ServiceFabricApplicationPackage", "New-ASLaunchConfiguration", "Set-ASInstanceHealth", "Get-EC2Image", "Get-EC2Instance", "Get-EC2NetworkInterface", "Get-ELB2LoadBalancer", "Get-ELB2TargetGroup", "Get-ElbHealthcheckEndpoints", "Enter-ASStandby", "Exit-ASStandby", "Get-ASAutoScalingGroup", "Get-ASAutoScalingInstance", "Get-ASInstanceHealth", "Get-ASLaunchConfiguration", "Get-SSMParameter" ,"Read-S3Object","Get-ELB2TargetHealth"}; + + static string[] serviceFabricFunctions = new[] { "Get-ServiceFabricApplication", "Get-ServiceFabricApplicationType", "Get-ServiceFabricApplicationUpgrade", "Get-ServiceFabricClusterConfiguration", "Get-ServiceFabricClusterConfigurationUpgradeStatus", "Get-ServiceFabricDeployedApplication", "Get-ServiceFabricDeployedCodePackage", "Get-ServiceFabricNode", "Get-ServiceFabricPartition", "Get-ServiceFabricRegisteredClusterCodeVersion", "Get-ServiceFabricService", "Start-ServiceFabricApplicationUpgrade", "Start-ServiceFabricClusterConfigurationUpgrade", "Start-ServiceFabricClusterUpgrade", "Unregister-ServiceFabricApplicationType", "Update-ServiceFabricService", "Remove-ServiceFabricApplication", "Remove-ServiceFabricCluster", "Register-ServiceFabricApplicationType", "Restart-ServiceFabricDeployedCodePackage", "New-ServiceFabricApplication", "New-ServiceFabricNodeConfiguration", "Connect-ServiceFabricCluster","Invoke-MinimizeEnvironment" }; + + static string[] cloudbase_powershell_yaml = new[] { "ConvertFrom-Yaml", "ConvertTo-Yaml" }; + + static string[] redisFunctions = new[] { "Invoke-RedisScript","Add-RedisKey" }; + + /// + /// Most of these fell out of comments because of the matching on verb-word + /// + static string[] ignoreRandomPhrases = new[] { "add-capability", "install-to", "install-type", "Install-To", "Write-out", "read-only", "test-case", "out-of", "use-case", "use-case", "out-of", "Request-Monitor", "read-to", "read-from", "read-only", "Select-Alkami",/*Used as demo files when generating a module.*/"Get-HelloWorld", "Get-HelloWorldInternal", /*Two aliases that are harder to revert than I expected*/"New-AlkamiWebAppPool", "Get-AlkamiWebAppPool","new-relic","open-block","exit-condition" }; + + static List excludeNames = new List { "VariableDeclarations", "ConfigurationValues" }; + + const string CarbonModuleName = "Carbon"; + const string WebAdministrationModuleName = "WebAdministration"; + const string WindowsModuleName = "Generic Windows"; + const string AwsModuleName = "AWS"; + const string CloudbaseModuleName = "Cloudbase"; + const string RedisModuleName = "Redis"; + const string ServiceFabricModuleName = "ServiceFabric"; + const string IgnoredFunctionsModuleName = "ignore anything with this module name"; + #endregion static decls + + + public static IEnumerable GetFunctionDefinitionsFromModule(DirectoryInfo directoryInfo, ModuleDefinition module, Regex regex) { + var functionDefinitions = new List(); + + var publicFiles = directoryInfo.GetDirectories("Public").FirstOrDefault(); + + if (publicFiles != null) { + foreach (var file in publicFiles.GetFiles("*.ps1")) { + var filename = Path.GetFileNameWithoutExtension(file.FullName); + if ( + !(filename.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase)) + && filename.IndexOf("-") > 1 + && verbs.Contains(filename.Split('-')[0]) + ) { + var referencedFunctions = regex.Matches(File.ReadAllText(file.FullName)).OfType().SelectMany(m => m.Captures.OfType().ToArray().Select(x => x.Value)).Distinct().Where(x => x.ToLowerInvariant() != filename.ToLowerInvariant()); + var functionDefinition = new ModuleFunctionDefinition(filename, module, referencedFunctions); + functionDefinitions.Add(functionDefinition); + } else { + if ( + !(filename.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase)) + && !excludeNames.Contains(filename) + ) { + Console.WriteLine(string.Format("Did not match [{0}] as a valid filename.", file.Name)); + } + } + } + } + + var privateFiles = directoryInfo.GetDirectories("Private").FirstOrDefault(); + + if (privateFiles != null) { + foreach (var file in privateFiles.GetFiles("*.ps1")) { + var filename = Path.GetFileNameWithoutExtension(file.FullName); + if ( + !(filename.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase)) + && filename.IndexOf("-") > 1 + && verbs.Contains(filename.Split('-')[0])) { + var referencedFunctions = regex.Matches(File.ReadAllText(file.FullName)).OfType().SelectMany(m => m.Captures.OfType().ToArray().Select(x => x.Value)).Distinct(); + var functionDefinition = new ModuleFunctionDefinition(filename, module, referencedFunctions) { IsPrivate = true }; + functionDefinitions.Add(functionDefinition); + } else { + if ( + !(filename.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) || filename.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase)) + && !excludeNames.Contains(filename) + ) { + Console.WriteLine(string.Format("Did not match [{0}] as a valid filename.", file.Name)); + } + } + } + } + + return functionDefinitions; + } + + + public static bool DoIt(string searchPath, bool showObviousCycles = false) { + var matchVerbsString = string.Join("|", verbs); + var matchString = string.Format("({0})-\\w+",matchVerbsString); + var regex = new Regex(matchString, RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + + var allFoundFunctions = new List(); + var allModules = new List(); + + #region pre-load ServiceFabric for reference + var sf = new ModuleDefinition(ServiceFabricModuleName) { IsStub = true, IsBase = true }; + foreach (var function in serviceFabricFunctions) { + var fd = new ModuleFunctionDefinition(function, sf); + sf.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load Carbon for reference + + #region pre-load AWS for reference + var aws = new ModuleDefinition(AwsModuleName) { IsStub = true, IsBase = true }; + foreach (var function in awsFunctions) { + var fd = new ModuleFunctionDefinition(function, aws); + aws.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load AWS for reference + + #region pre-load Carbon for reference + var carbon = new ModuleDefinition(CarbonModuleName) { IsBase = true }; + foreach (var function in carbonFunctions) { + var fd = new ModuleFunctionDefinition(function, carbon); + carbon.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load Carbon for reference + + #region pre-load WebAdministration for reference + var webAdministration = new ModuleDefinition(WebAdministrationModuleName) { IsBase = true }; + foreach (var function in webAdministrationFunctions) { + var fd = new ModuleFunctionDefinition(function, webAdministration); + webAdministration.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load WebAdministration for reference + + #region pre-load Windows for reference + var windows = new ModuleDefinition(WindowsModuleName) { IsStub = true, IsBase = true }; + foreach (var function in windowsFunctions) { + var fd = new ModuleFunctionDefinition(function, windows); + windows.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load Windows for reference + + #region pre-load ignored functions for reference + var ignored = new ModuleDefinition(IgnoredFunctionsModuleName) { IsStub = true, IsBase = true }; + foreach (var function in ignoreRandomPhrases) { + var fd = new ModuleFunctionDefinition(function, ignored); + ignored.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load Ignored functions for reference + + #region pre-load cloudbase for reference + var cloudbase = new ModuleDefinition(CloudbaseModuleName) { IsStub = true, IsBase = true }; + foreach (var function in cloudbase_powershell_yaml) { + var fd = new ModuleFunctionDefinition(function, cloudbase); + windows.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load cloudbase for reference + + #region pre-load redis for reference + var redis = new ModuleDefinition(RedisModuleName) { IsStub = true, IsBase = true }; + foreach (var function in redisFunctions) { + var fd = new ModuleFunctionDefinition(function, redis); + aws.AddFunction(fd); + allFoundFunctions.Add(fd); + } + #endregion pre-load redis for reference + + var modules = new DirectoryInfo(searchPath).GetFiles("*.psd1", SearchOption.AllDirectories); + + foreach (var module in modules) { + var moduleName = Path.GetFileNameWithoutExtension(module.FullName); + var isCsharp = false; + if (module.Directory.GetFiles("*.csproj").Any()) { + Console.WriteLine(string.Format("Found a CS Project for {0}",moduleName)); + isCsharp = true; + } + + var moduleDefinition = new ModuleDefinition(moduleName) { IsCsharp = isCsharp }; + var functionDefinitions = GetFunctionDefinitionsFromModule(module.Directory, moduleDefinition, regex); + moduleDefinition.AddFunctions(functionDefinitions); + allFoundFunctions.AddRange(functionDefinitions); + + allModules.Add(moduleDefinition); + } + + // Now that all the modules have been parsed and we have all the function definitions, we need to do something with the functions + // Let's parse the functions to find all the places they are used, and see what that web looks like. + // So basically, foreach function find modules they need to function. + // There may be more than one function defined in a given module, example: Get-Certificate in carbon and alkami.ops.certificates + + Dictionary refDic = new Dictionary(); + foreach (var function in allFoundFunctions) { + if (!refDic.ContainsKey(function.Name.ToLower())) { + refDic.Add(function.Name.ToLower(), function); + } + } + foreach (var module in allModules) { + foreach (var function in module.ModuleFunctionDefinitions) { + foreach (var name in function.ReferencedFunctions) { + if (function.ReferencedFunctions.Contains(name) && function.Name != name) { + try { + refDic[name.ToLower()].Add(function, module); + if (!function.MyFunctions.Contains(refDic[name.ToLower()])) { + function.MyFunctions.Add(refDic[name.ToLower()]); + } + } catch (Exception) { + Console.WriteLine(string.Format("Could not find a parent-module for {0} in {1}",name,function.Name)); + } + } + } + } + } + + Console.WriteLine(""); + Console.WriteLine("======================"); + Console.WriteLine(""); + + var shouldThrow = false; + + foreach (var module in allModules.Where(x => !x.IsStub)) { + if (module.RelatedModules.Where(x => !x.IsStub).Any()) { + Console.WriteLine(string.Format("{0} uses",module.Name)); + foreach (var subModule in module.RelatedModules.Where(x => !x.IsStub)) { + var isWrongOrder = (module.IsBase && !subModule.IsBase); + var isObviousCycle = subModule.RelatedModules.Contains(module); + if (isWrongOrder) { + Console.WriteLine(string.Format(" * {0} - WARNING - SOMETHING REQUIRES A HIGHER-ORDER-FUNCTION", subModule.Name)); + var exceptions = subModule.ModuleFunctionDefinitions.Where(x => x.ReferencedBy.Where(y => y.Module == module).Any()).Select(x => new { Name = x.Name, Where = x.ReferencedBy.Where(t => t.Module == module).Select(t => t.Name).ToArray() }); + foreach (var exception in exceptions) { + foreach (var functionName in exception.Where) { + Console.WriteLine(string.Format(" * !! {0} calls {1}",functionName,exception.Name)); + } + } + shouldThrow = true; + } else if (isObviousCycle && showObviousCycles) { + Console.WriteLine(string.Format(" * {0} - WARNING - OBVIOUS CYCLE",subModule.Name)); + var exceptions = subModule.ModuleFunctionDefinitions.Where(x => x.ReferencedBy.Where(y => y.Module == module).Any()).Select(x => new { Name = x.Name, Where = x.ReferencedBy.Where(t => t.Module == module).Select(t => t.Name).ToArray() }); + foreach (var exception in exceptions) { + foreach (var functionName in exception.Where) { + Console.WriteLine(string.Format(" * !! {0} calls {1}",functionName,exception.Name)); + } + } + shouldThrow = true; + } else { + Console.WriteLine(string.Format(" * {0}",subModule.Name)); + } + } + } else { + Console.WriteLine(string.Format("{0} doesn't have additional-module usage",module.Name)); + } + Console.WriteLine(""); + } + return (shouldThrow); + } + } + + public class ModuleDefinition { + public string Name { get; set; } + public List ModuleFunctionDefinitions { get; set; } + public List RelatedModules { get; set; } + public int Weight { get; set; } + public bool IsCsharp { get; set; } + /// + /// This flag means we are ignoring this "module" as it is only for comparison purposes + /// + public bool IsStub { get; set; } + + /// + /// This means it's Alkami.PowerShell.* or something like it + /// + public bool IsBase { get; set; } + + public ModuleDefinition() { + ModuleFunctionDefinitions = new List(); + RelatedModules = new List(); + Weight = 0; + } + + public ModuleDefinition(string name) { + Name = name; + ModuleFunctionDefinitions = new List(); + RelatedModules = new List(); + Weight = 0; + IsBase = name.ToLower().StartsWith("Alkami.PowerShell".ToLower()); + } + + public ModuleDefinition(string name, IEnumerable relatedModuleNames) { + Name = name; + ModuleFunctionDefinitions = new List(); + RelatedModules = new List(); + RelatedModules.AddRange(relatedModuleNames); + Weight = 0; + IsBase = name.ToLower().StartsWith("Alkami.PowerShell".ToLower()); + } + + public ModuleDefinition AddFunction(ModuleFunctionDefinition functionDefinition) { + ModuleFunctionDefinitions.Add(functionDefinition); + return this; + } + + public ModuleDefinition AddFunctions(IEnumerable functionDefinitions) { + ModuleFunctionDefinitions.AddRange(functionDefinitions); + return this; + } + + public override bool Equals(Object obj) { + //Check for null and compare run-time types. + if ((obj == null) || !this.GetType().Equals(obj.GetType())) { + return false; + } else { + ModuleDefinition moduleDefinition = (ModuleDefinition)obj; + return Name == moduleDefinition.Name; + } + } + + public override int GetHashCode() { + return Name.GetHashCode(); + } + + public override string ToString() { + return string.Format("ModuleDefintion: {0}",Name); + } + } + + public class FunctionDefinition { + public string Name { get; set; } + public ModuleDefinition Module { get; set; } + public bool IsPrivate { get; set; } + + public FunctionDefinition() { + } + + public FunctionDefinition(string name) { + Name = name; + } + + public FunctionDefinition(string name, ModuleDefinition module) { + Name = name; + Module = module; + } + + public override bool Equals(Object obj) { + //Check for null and compare run-time types. + if ((obj == null) || !this.GetType().Equals(obj.GetType())) { + return false; + } else { + FunctionDefinition functionDefinition = (FunctionDefinition)obj; + return Name == functionDefinition.Name; + } + } + + public override int GetHashCode() { + return Name.GetHashCode(); + } + + public override string ToString() { + return string.Format("ModuleFunctionDefinition: {0} - ModuleDefinition {1}",Name,Module.Name); + } + } + + public class ModuleFunctionDefinition : FunctionDefinition { + public List ReferencedFunctions { get; set; } + + public ModuleFunctionDefinition() : this(null) { } + + public ModuleFunctionDefinition(string name) : this(name, null) { } + + public ModuleFunctionDefinition(string name, ModuleDefinition module) : this(name, module, null) { } + + public ModuleFunctionDefinition(string name, ModuleDefinition module, IEnumerable referencedFunctions) : base(name, module) { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException("name"); + if (module == null) throw new ArgumentNullException("module"); + + ReferencedFunctions = new List(); + if (referencedFunctions != null) { + ReferencedFunctions.AddRange(referencedFunctions); + } + + ReferencedBy = new List(); + ReferencedIn = new List(); + MyFunctions = new List(); + } + + public FunctionDefinition Add(string referencedFunction) { + ReferencedFunctions.Add(referencedFunction); + return this; + } + + public override bool Equals(Object obj) { + //Check for null and compare run-time types. + if ((obj == null) || !this.GetType().Equals(obj.GetType())) { + return false; + } else { + FunctionDefinition functionDefinition = (FunctionDefinition)obj; + return Name == functionDefinition.Name; + } + } + + public override int GetHashCode() { + return Name.GetHashCode(); + } + + public override string ToString() { + return string.Format("ModuleFunctionDefinition: {0} - ModuleDefinition {1}",Name,Module.Name); + } + + public void Add(ModuleFunctionDefinition functionDefinition, ModuleDefinition module) { + if (!ReferencedBy.Contains(functionDefinition)) { ReferencedBy.Add(functionDefinition); } + if (!ReferencedIn.Contains(module)) { ReferencedIn.Add(module); } + if (Module != module && !module.RelatedModules.Contains(Module)) { module.RelatedModules.Add(Module); } + } + + // These are the functions that reference me + public List ReferencedBy { get; set; } + // These are the functions that I reference + public List ReferencedIn { get; set; } + + public List MyFunctions { get; set; } + } +"@; + +Write-Host "testing solution" + +Add-Type -TypeDefinition $cSharpCode; + +$result = [ModuleChecker]::DoIt($FolderPath, $ShowObviousCycles) + + if ($result) { + if ($FailBuild) { + throw "Failed processing, check output for bad module definitions" + } else { + Write-Warning "Failed processing, check output for bad module definitions" + } + } + +} \ No newline at end of file diff --git a/Modules/.build/Update-AliasesInPSD1.ps1 b/Modules/.build/Update-AliasesInPSD1.ps1 new file mode 100644 index 0000000..405c4cb --- /dev/null +++ b/Modules/.build/Update-AliasesInPSD1.ps1 @@ -0,0 +1,65 @@ +Function Update-AliasesInPSD1 { +<# +.SYNOPSIS + Concatenates all the aliases from Get-Aliases for insertion into the PSD1 file referenced. + +.EXAMPLE + Update-AliasesInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1 + +.PARAMETER FolderPath + The name of the folder to examine all files under. + +.PARAMETER Psd1FilePath + The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.) +#> + [CmdletBinding()] + Param ( + [String]$FolderPath, + [String]$Psd1FilePath + ) + process { + $aliasNames = @(Get-Aliases $FolderPath) | Sort-Object + + if ($aliasNames.Count -eq 0) { + return + } + + $toBeJoinedAliases = @() + + foreach($name in $aliasNames) { + $toBeJoinedAliases += "'$name'" + } + + $joinedAliases = $toBeJoinedAliases -Join ',' + + $contents = (Get-Content $Psd1FilePath) + $newContents = @() + + $foundAliasesLine = $false + foreach($line in $contents) { + if ($line.Trim().StartsWith("AliasesToExport")) { + $splits = $line -split '=' + $line = $splits[0].TrimEnd() + " = $joinedAliases" + $foundAliasesLine = $true + } + $newContents += $line + } + + if (!$foundAliasesLine) { + $newContents = @() + ## We couldn't find an existing aliases line in that PSD1, so we need to add one. + ## We are gonna add it after FunctionsToExport + foreach($line in $contents) { + $newContents += $line + if ($line.Trim().StartsWith("FunctionsToExport")) { + $splits = $line -split '=' + $newLine = $splits[0].TrimEnd().Replace('FunctionsToExport','AliasesToExport') + " = $joinedAliases" + + $newContents += $newLine + } + } + } + + Set-Content -Path $Psd1FilePath -Value $newContents + } +} \ No newline at end of file diff --git a/Modules/.build/Update-FormatsToProcessInPSD1.ps1 b/Modules/.build/Update-FormatsToProcessInPSD1.ps1 new file mode 100644 index 0000000..f86a0cc --- /dev/null +++ b/Modules/.build/Update-FormatsToProcessInPSD1.ps1 @@ -0,0 +1,61 @@ +Function Update-FormatsToProcessInPSD1 { +<# +.SYNOPSIS + Updates the PSD1 FormatsToProcess entry + +.EXAMPLE + Update-FormatsToProcessInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1 + +.PARAMETER FolderPath + The name of the folder to examine all files under. + +.PARAMETER Psd1FilePath + The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.) +#> + [CmdletBinding()] + Param ( + [String]$FolderPath, + [String]$Psd1FilePath + ) + process { + # In the future, may want to ship all the format files separately + # Currently there is Get-ContentFromFormatFilesInPath and Join-PS1XMLFromFiles that smacks these two together + # So for now, we just want to write out the name of the built ps1xml to the psd1 + # The names are the same, so that's easy. + + $contents = (Get-Content $Psd1FilePath) + $newContents = @() + + $projectName = [System.IO.Path]::GetFileNameWithoutExtension($psd1Filepath) + $magicWord = 'FormatsToProcess' + $magicConcat = " = `"$projectName.ps1xml`"" + + $foundFormatsLine = $false + foreach($line in $contents) { + if ($line.Trim().StartsWith($magicWord)) { + $splits = $line -split '=' + $line = $splits[0].TrimEnd() + $magicConcat + $foundFormatsLine = $true + } + $newContents += $line + } + + if (!$foundFormatsLine) { + $newContents = @() + ## We couldn't find an existing formats line in that PSD1, so we need to add one. + ## We are gonna add it after FunctionsToExport + $duplicatableTag = 'FunctionsToExport' + foreach($line in $contents) { + $newContents += $line + if ($line.Trim().StartsWith($duplicatableTag)) { + $splits = $line -split '=' + $newLine = $splits[0].TrimEnd().Replace($duplicatableTag,$magicWord) + $magicConcat + + $newContents += $newLine + } + } + } + + Set-Content -Path $Psd1FilePath -Value $newContents + } +} \ No newline at end of file diff --git a/Modules/.build/Update-FunctionNamesInPSD1.ps1 b/Modules/.build/Update-FunctionNamesInPSD1.ps1 new file mode 100644 index 0000000..c6dc050 --- /dev/null +++ b/Modules/.build/Update-FunctionNamesInPSD1.ps1 @@ -0,0 +1,43 @@ +Function Update-FunctionNamesInPSD1 { +<# +.SYNOPSIS + Concatenates all the function names from Get-FunctionNames for insertion into the PSD1 file referenced. + +.EXAMPLE + Update-FunctionNamesInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1 + +.PARAMETER FolderPath + The name of the folder to examine all files under. + +.PARAMETER Psd1FilePath + The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.) +#> + [CmdletBinding()] + Param ( + [String]$FolderPath, + [String]$Psd1FilePath + ) + process { + $functionNames = @(Get-FunctionNames $FolderPath) | Sort-Object + + $toBeJoinedFunctionNames = @() + + foreach($name in $functionNames) { + $toBeJoinedFunctionNames += "'$name'" + } + + $joinedFunctionNames = $toBeJoinedFunctionNames -Join ',' + + $contents = (Get-Content $Psd1FilePath) + $newContents = @() + + foreach($line in $contents) { + if ($line.Trim().StartsWith("FunctionsToExport")) { + $splits = $line -split '=' + $line = $splits[0].TrimEnd() + " = $joinedFunctionNames" + } + $newContents += $line + } + Set-Content -Path $Psd1FilePath -Value $newContents + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.nuspec b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.nuspec new file mode 100644 index 0000000..c9aca29 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.nuspec @@ -0,0 +1,34 @@ + + + + Alkami.DevOps.Certificates + $version$ + Alkami Platform Modules - DevOps - Certificates + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Certificates + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps Certificates module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.psd1 b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.psd1 new file mode 100644 index 0000000..35328f0 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.psd1 @@ -0,0 +1,21 @@ +@{ + RootModule = 'Alkami.DevOps.Certificates.psm1' + ModuleVersion = '3.20.11' + GUID = 'bf9b5927-92e8-4eff-b8f3-c19cc554e4c2' + Author = 'SRE' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions used to deploy the ORB application' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Services','Alkami.Ops.Common','Alkami.PowerShell.IIS','Alkami.Ops.SecretServer' + FunctionsToExport = 'Compress-Certificates','Export-Certificates','Export-CertificatesToFileSystem','Get-ExpiringCertificates','Get-PrivateKeyPermissions','Get-SecretServerConnection','Import-Certificates','Import-PfxCertificateWithPermissions','Import-PodFromSecretServer','Publish-PodToSecretServer','Read-AppTierCertificates','Read-Certificates','Read-WebTierCertificates','Remove-Certificate','Save-CertificatesToDisk','Update-CertBindings' + AliasesToExport = 'Load-AppTierCertificates','Load-Certificates','Load-WebTierCertificates' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'deploy', 'deployment') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Certificate+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Certificate+Module' +} diff --git a/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.pssproj b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.pssproj new file mode 100644 index 0000000..c41c41b --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Alkami.DevOps.Certificates.pssproj @@ -0,0 +1,78 @@ + + + + Debug + 2.0 + {90fa52ce-26c5-44f0-a0c5-0ac3355e6fdc} + Exe + MyApplication + MyApplication + Alkami.DevOps.Certificates + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Certificates") + Invoke-Pester; + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/AlkamiManifest.xml b/Modules/Alkami.DevOps.Certificates/AlkamiManifest.xml new file mode 100644 index 0000000..195e5c8 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Certificates + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Certificates/Private/Confirm-Cert.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Confirm-Cert.ps1 new file mode 100644 index 0000000..74faced --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Confirm-Cert.ps1 @@ -0,0 +1,46 @@ +function Confirm-Cert { +<# +.SYNOPSIS + Validates that Certificate is provided. +#> + + [CmdletBinding()] + Param( + + [Parameter(Mandatory=$true)] + [string]$certName, + + [parameter(Mandatory=$true)] + [System.Security.Cryptography.X509Certificates.StoreName]$storeName + ) + + try + { + $OriginalErrorActionPreference = $ErrorActionPreference; + $ErrorActionPreference = "Continue"; + + Write-Output ("Validating certificate $certName"); + + [Alkami.Ops.Common.Cryptography.CertificateHelper]::ValidateCertificate( + $certName, + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine); + + Write-Output ("Certificate $certName Passed Validation"); + } + catch [Alkami.Ops.Common.Exceptions.InvalidCertificateException] + { + Write-Warning ("Certificate validation failed"); + Write-Host (" Error: " + $_.Exception.Message); + Write-Host (" Name: " + $_.Exception.CertificateName); + Write-Host (" Thumbprint: " + $_.Exception.CertificateThumbPrint); + Write-Host (" Effective Date: " + $_.Exception.EffectiveDateTime); + Write-Host (" Expiration Date: " + $_.Exception.ExpirationDateTime + "`n"); + } + finally + { + $ErrorActionPreference = $OriginalErrorActionPreference; + } +} + + diff --git a/Modules/Alkami.DevOps.Certificates/Private/Export-Cert.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Export-Cert.ps1 new file mode 100644 index 0000000..a1c15f2 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Export-Cert.ps1 @@ -0,0 +1,37 @@ +function Export-Cert { +<# +.SYNOPSIS + Exports a Certificate. +#> + + [CmdletBinding()] + [OutputType([System.Object])] + Param( + + [parameter(Mandatory=$true)] + [string]$exportPath, + + [parameter(Mandatory=$false)] + [string]$exportPassword, + + [parameter(Mandatory=$true)] + [System.Security.Cryptography.X509Certificates.StoreName]$storeName + ) + + if ($exportPassword) + { + return ,[Alkami.Ops.Common.Cryptography.CertificateHelper]::ExportAllCertificates( + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine, + $exportPath, + $exportPassword + ); + } + + return ,[Alkami.Ops.Common.Cryptography.CertificateHelper]::ExportAllCertificates( + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine, + $exportPath + ); +} + diff --git a/Modules/Alkami.DevOps.Certificates/Private/Export-CertChain.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Export-CertChain.ps1 new file mode 100644 index 0000000..afd0deb --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Export-CertChain.ps1 @@ -0,0 +1,36 @@ +function Export-CertChain { +<# +.SYNOPSIS + Exports a Certificate's Chain. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [ValidateNotNull()] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert, + [Parameter(Mandatory = $True)] + [string]$ExportStorePath, + [Parameter(Mandatory = $True)] + [string]$ExportCertPath, + $ADGroups + ) + $certName = $exportCertPath.Split("\") | Select-Object -Last 1 + $chain = Get-CertificateChain $cert $exportStorePath + $chainInfo = [System.Collections.ArrayList]::new() + foreach ($chainCert in $chain) { + + $chainCertStore = Get-CertificateStoreName $chainCert + if (!$chainCertStore) { + Write-Warning "Chain is broken for cert $certName and thumbprint $($chainCert.thumbprint)" + break + } + $exportChainPath = $exportCertPath, "ChainedCertificates", $chainCertStore -join "\" + + $exportInfo = Export-CertificateToFileSystem $chainCert $exportChainPath -IsChainExport $true -ADGroups $ADGroups + if ($null -eq $exportInfo) {break} + + [void]$chainInfo.Add($exportInfo) + } + + return $chainInfo +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Export-CertificateToFileSystem.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Export-CertificateToFileSystem.ps1 new file mode 100644 index 0000000..8ed596f --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Export-CertificateToFileSystem.ps1 @@ -0,0 +1,48 @@ +function Export-CertificateToFileSystem { +<# +.SYNOPSIS + Exports a Certificate to a Store. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [ValidateNotNull()] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert, + [Parameter(Mandatory = $True)] + [ValidateNotNull()] + [string]$ExportStorePath, + [bool]$IsChainExport = $false, + [string[]]$ADGroups + ) + $certName = Get-CertificateExportName $cert + $exportCertPath = if ($IsChainExport) {$exportStorePath}else {Join-Path $exportStorePath $certName} + + $exportInfo = Get-CertificateExportInfo $cert $exportCertPath + if ($exportInfo.certExportType -eq [System.Security.Cryptography.X509Certificates.X509ContentType]::Unknown) {return $null} + + if (-Not (Test-Path $exportCertPath -PathType Container)) { + New-Item $exportCertPath -ItemType Directory | Out-Null + } + + $exportInfo.certName = $certName + $exportInfo.certPassword = ([char[]]([char]33..[char]95) + ([char[]]([char]97..[char]126)) + 0..9 | Sort-Object {Get-Random})[0..128] -join '' + $exportInfo.ADGroups = $ADGroups + + if ($exportInfo.certExportType -eq [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx) { + $password = $exportInfo.certPassword | ConvertTo-SecureString -AsPlainText -Force + try { + Export-PfxCertificate -Cert $cert -ProtectTo $ADGroups -FilePath $exportInfo.exportCertFile -Password $password + } + catch { + Write-Warning "Certificate $certName with thumbprint $($cert.Thumbprint) but could not be exported + $($_.Exception.Message)" + return $null + } + } + else { + $certBytes = $cert.Export($exportInfo.certExportType) + [void][io.file]::WriteAllBytes($exportInfo.exportCertFile, $certBytes) + } + + return $exportInfo +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Get-Cert.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Get-Cert.ps1 new file mode 100644 index 0000000..358323c --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Get-Cert.ps1 @@ -0,0 +1,36 @@ +function Get-Cert { +<# +.SYNOPSIS + Fetches a Certificate. +#> + [CmdletBinding()] + param( + [string]$Thumbprint, + [string]$StoreName, + [string]$FriendlyName + ) + + $certStore = @{ } + $certStores = Get-ChildItem Cert:\LocalMachine\ | ForEach-Object { "Cert:\LocalMachine\$($_.Name)" } + if ($StoreName) { + $certStores = $certStores | Where-Object { $_ -Match "\\$StoreName" } + } + foreach ($store in $certStores) { + Get-ChildItem $store | Where-Object { $_.NotAfter -gt (Get-Date) } | ForEach-Object { + if ($certStore.ContainsKey($_.Thumbprint)) { + $certStore[$_.Thumbprint].Add($_) + } else { + $list = [System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate]]::new() + $list.Add($_) + $certStore.Add($_.Thumbprint, $list) + } + } + } + + if ($Thumbprint) { + return $certStore[$Thumbprint] + } elseif ($FriendlyName) { + return $certStore.Values | Where-Object { $_.FriendlyName -match $FriendlyName } + } + return $certStore.Values +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateChain.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateChain.ps1 new file mode 100644 index 0000000..464fbca --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateChain.ps1 @@ -0,0 +1,14 @@ +function Get-CertificateChain { +<# +.SYNOPSIS + Fetches a Certificate's Chain. +#> + [CmdletBinding()] + param( + [Parameter()] + $Cert + ) + $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() + [void]$chain.Build($cert) + return $chain.ChainElements.Certificate | Select-Object -Skip 1 +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportInfo.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportInfo.ps1 new file mode 100644 index 0000000..a4141b9 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportInfo.ps1 @@ -0,0 +1,37 @@ +function Get-CertificateExportInfo { +<# +.SYNOPSIS + Fetches a Certificate's Export Information. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert, + [Parameter(Mandatory = $true)] + [string]$ExportCertPath) + + $exportInfo = [PSCustomObject]@{ + CertExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx + ExportCertFile = Join-Path $exportCertPath "$certName.pfx" + ExportCertPath = $exportCertPath + CertPassword = "" + ADGroups = "" + CertName = "" + ExpirationDate = $cert.NotAfter + Thumbprint = $cert.Thumbprint + } + + if ($cert.HasPrivateKey) { + if (!$cert.PrivateKey.CspKeyContainerInfo.Exportable) { + Write-Warning "Certificate $certName with thumbprint $($cert.Thumbprint) has a private key but is marked as unexportable. + This certificate will not be exported" + $exportInfo.certExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Unknown + } + } + else { + $exportInfo.certExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert + $exportInfo.exportCertFile = Join-Path $exportCertPath "$certName.cer" + } + + return $exportInfo +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportName.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportName.ps1 new file mode 100644 index 0000000..7ae5ed7 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateExportName.ps1 @@ -0,0 +1,19 @@ +function Get-CertificateExportName { +<# +.SYNOPSIS + Fetches a Certificate's Export Name. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $Cert + ) + + $canonicalName = ($cert.Subject.Trim().Split(",") | Where-Object {$_ -match "CN="} | Select-Object -First 1 ) -replace "CN=", "" + $invalidFileNameChars = [IO.Path]::GetInvalidFileNameChars() -join '' + $validFileNameCN = ($canonicalName -replace ("[{0}]" -f [RegEx]::Escape($invalidFileNameChars))) + + $certName = if ($validFileNameCN) { $validFileNameCN } else { $cert.Thumbprint } + + return $certName.Trim() +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateStoreName.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateStoreName.ps1 new file mode 100644 index 0000000..309dbb9 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Get-CertificateStoreName.ps1 @@ -0,0 +1,15 @@ +function Get-CertificateStoreName { +<# +.SYNOPSIS + Fetches a Certificate's Store Name. +#> + [CmdletBinding()] + param( + [Parameter()] + $Cert + ) + $psParent = Get-Cert -Thumbprint $cert.Thumbprint | Select-Object -ExpandProperty PSParentPath + if (!$psParent) {return} + $storeName = $psParent.Split("\") | Select-Object -Last 1 + return $storeName +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Import-Cert.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Import-Cert.ps1 new file mode 100644 index 0000000..6f98880 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Import-Cert.ps1 @@ -0,0 +1,33 @@ +function Import-Cert { +<# +.SYNOPSIS + Imports a Certificate into a Store. +#> + + [CmdletBinding()] + Param( + + [Parameter(Mandatory=$true)] + [string]$certFullName, + + [parameter(Mandatory=$true)] + [System.Security.Cryptography.X509Certificates.StoreName]$storeName, + + [Parameter(Mandatory=$false)] + [string]$importPassword + ) + + if ($importPassword) + { + return [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore( + $certFullName, + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine, + $importPassword); + } + + return [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore( + $certFullName, + $storeName, + [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine); +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.ps1 b/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.ps1 new file mode 100644 index 0000000..c49fb80 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.ps1 @@ -0,0 +1,195 @@ +# Ignore Measure-HelpSynopsis warnings in the PSScript Analyzer +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami.PowerShell.PSScriptAnalyzerRules\Measure-HelpSynopsis', '', Scope = 'Class')] +# Ignore Measure-HelpSynopsis warnings in the PSScript Analyzer - PS Classes can't do CmdletBinding, afaict +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami.PowerShell.PSScriptAnalyzerRules\Measure-CmdletBinding', '', Scope = 'Class')] + +Class SecretServerConnection { + [string]$api + [string]$site + [string]$userName + [string]$domain + [string]$password + [string]$tokenRoute + [ScriptBlock]$commonFilters = { "?filter.includeRestricted=true&filter.searchtext=$searchString&filter.folderId=$folderId&filter.includeSubFolders=true" } + [ScriptBlock]$updateEndpoint + [System.Collections.Generic.Dictionary[[String], [String]]]$commonHeader + $token + + SecretServerConnection() { } + + SecretServerConnection([string]$site, [string]$userName, [string]$password) { + $this.site = $site + $this.tokenRoute = "$site/oauth2/token" + $this.api = $site, "api/v1" -join "/" + $this.updateEndpoint = { "/secrets/$secretId/fields/$fieldToUpdate" } + $this.userName = $userName + $this.password = $password + $this.commonHeader = [System.Collections.Generic.Dictionary[[String], [String]]]::new() + } + + [void]Authenticate() { + $this.Authenticate($False) + } + [void]Authenticate([bool]$UseTwoFactor) { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $creds = @{ + username = $this.username + password = $this.password + grant_type = "password" + } + + $headers = $null + If ($UseTwoFactor) { + $headers = @{ + "OTP" = (Read-Host -Prompt "Enter your OTP for 2FA: ") + } + } + try { + $response = Invoke-RestMethod $this.tokenRoute -Method Post -Body $creds -Headers $headers + $this.token = $response.access_token; + if ($this.commonHeader.Count -gt 0) { $this.commonHeader.Clear() } + $this.commonHeader.Add("Authorization", "Bearer $($this.token)") + } catch { + throw $_ + } + } + [object]GetSecretByName([string]$secretName, [string]$folderId) { + # $searchString is used in $commonFilters above. This "not used" warning is a lie. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False Positive')] + $searchString = $secretName + $filters = $this.commonFilters.Invoke() + Write-Debug "$($this.api)/secrets/lookup$filters" + $result = Invoke-RestMethod "$($this.api)/secrets$filters" -Headers $this.commonHeader + + return $result + } + + [object]GetSecretById([string]$secretId) { + + Write-Debug "$($this.api)/secrets/$secretId" + $response = Invoke-RestMethod "$($this.api)/secrets/$secretId" -Headers $this.commonHeader + + return $response + } + [object]GetSecretByFolderId([string]$folderId) { + $parameters = "?filter.folderId=$folderId" + + $response = Invoke-RestMethod "$($this.api)/secrets/$parameters" -Headers $this.commonHeader + + return $response.records + } + + [object]GetSecretTemplateById([int]$templateId, [int]$folderId) { + $secret = Invoke-RestMethod "$($this.api)/secrets/stub?filter.secrettemplateid=$templateId&filter.folderId=$folderId" -Headers $this.commonHeader + + return $secret + } + + [int]GetSecretTemplateIdByName([string]$templateName) { + $searchString = "?filter.searchText=$templateName" + + $secret = Invoke-RestMethod -Method Get "$($this.api)/secret-templates$searchString" -Headers $this.commonHeader + + if ($secret.records) { + $record = $secret.records | Where-Object { $_.name -eq $templateName } + return $record.id; + } + + return -1 + } + + [int]CreateSecret([object]$secret, [int]$folderId, [string]$secretName) { + + $secret.name = $secretName + $secret.siteId = 1 + $secret.folderId = $folderId + + # Get Secret Template first, set up the template with various items and their values and then pass it to create secret + $requestCreateSecretParams = $secret | ConvertTo-Json + $secret = Invoke-RestMethod "$($this.api)/secrets/" -Method Post -Body $requestCreateSecretParams -Headers $this.commonHeader -ContentType "application/json" + + return $secret.id + } + [object]UpdateField([string]$secretId, [string]$fieldToUpdate, [string]$newValue) { + $body = @{ value = $newValue } | ConvertTo-Json + $response = Invoke-RestMethod -Method Put -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -ContentType "application/json" -Body $body + return $response + } + + [object]GetField([string]$secretId, [string]$field) { + $response = Invoke-RestMethod -Method Get -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader + return $response.Records + } + + [void]UploadFile([int]$secretId, [string]$fieldToUpdate, [string]$filePath) { + + $fileName = Get-ChildItem $filePath | Select-Object -ExpandProperty Name + + $requestUploadFileParams = @{ + fileName = $fileName; + fileAttachment = [IO.File]::ReadAllBytes($filePath) + } | ConvertTo-Json + + Invoke-RestMethod -Method Put -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -Body $requestUploadFileParams -ContentType "application/json" + + } + + [bool]DownloadFile([int]$secretId, [string]$fieldToupdate, [string]$filePath) { + # invokeing $this.updateEndpoint.Invoke() sets variables hidden above in SecretServerConnection + Write-Debug "$($this.api)$($this.updateEndpoint.Invoke())" + try { + Invoke-RestMethod -Method Get -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -OutFile $filePath | Out-Null + return $true + } catch { + Write-Warning "An error occurred while downloading a file." + Write-Warning -Message "StatusCode: $($_.Exception.Response.StatusCode.value__)" + Write-Warning -Message "StatusDescription: $($_.Exception.Response.StatusDescription)" + return $false + } + } + + [int]AddFolder([int]$parentFolderId, [string]$folderName, [bool]$inheritPermissions, [bool]$inheritSecretPolicy) { + + $folderStub = Invoke-RestMethod "$($this.api)/folders/stub" -Method GET -Headers $this.commonHeader -ContentType "application/json" + + $folderStub.folderName = $folderName + $folderStub.folderTypeId = 1 + $folderStub.inheritPermissions = $inheritPermissions + $folderStub.inheritSecretPolicy = $inheritSecretPolicy + $folderStub.parentFolderId = $parentFolderId + + $folderArgs = $folderStub | ConvertTo-Json + + $folderAddResult = Invoke-RestMethod "$($this.api)/folders" -Method POST -Body $folderArgs -Headers $this.commonHeader -ContentType "application/json" + + return $folderAddResult.id + } + [int]AddFolder([int]$parentFolderId, [string]$folderName) { + return $this.AddFolder($parentFolderId, $folderName, $true, $true) + } + [object]GetFolderById([int]$folderId) { + $folderGetResult = Invoke-RestMethod "$($this.api)/folders/$folderId" -Method GET -Headers $this.commonHeader -ContentType "application/json" + return $folderGetResult.records + } + [object]GetChildFolders([int]$parentId) { + $parameters = "?filter.parentFolderId=$parentId" + $folderGetResult = Invoke-RestMethod "$($this.api)/folders/$parameters" -Method GET -Headers $this.commonHeader -ContentType "application/json" + return $folderGetResult.records + } + [object]GetFolderIdByName([string]$folderName) { + $searchFilter = "?filter.searchText=$folderName" + + $searchResults = Invoke-RestMethod "$($this.api)/folders$searchFilter" -Method GET -Headers $this.commonHeader -ContentType "application/json" + + return $searchResults.Records.Id + } + [object]GetFolderIdByName([string]$folderName, [int]$parentFolderId) { + $searchString = $folderName + $folderId = $parentFolderId + $filters = "?filter.includeRestricted=true&filter.parentFolderId=$folderId&filter.searchtext=%$searchString" + Write-Debug $filters + $searchResults = Invoke-RestMethod "$($this.api)/folders$filters" -Method GET -Headers $this.commonHeader -ContentType "application/json" + + return $searchResults.Records.Id + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.tests.ps1 b/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.tests.ps1 new file mode 100644 index 0000000..f361925 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/SecretServerConnection.tests.ps1 @@ -0,0 +1,61 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +#Import-Module $functionPath -Force +$moduleForMock = "" + +InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $moduleForMock = "" + $inScopeModuleForAssert = "Alkami.DevOps.Certificates" + + Describe "SecretServerConnection" { + Context "When Calling GetSecretByName" { + + Mock Invoke-RestMethod { + #Write-Warning "Mocked Invoke-RestMethod" + if ($uri -like "*CertName*123*") { + return New-Object psobject -Property @{ + Name = "I'm a fake Secret" + } + } else { + return $null + } + } -ModuleName $moduleForMock + + It "Returns Secrets Which Match The Supplied Name And FolderId" { + + $connection = [SecretServerConnection]::new() + $result = $connection.GetSecretByName("CertName", 123) + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod + + $result.Name | should -BeLike "I'm a fake Secret" + } + + It "Does Not Return Secrets Which Do Not Match The Supplied Name" { + + $connection = [SecretServerConnection]::new() + $result = $connection.GetSecretByName("BadName", 123) + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod + + $result | should -BeNullOrEmpty + } + + + It "Does Not Return Secrets Which Do Match The Supplied Name But Not the Folder Id" { + + $connection = [SecretServerConnection]::new() + $result = $connection.GetSecretByName("CertName", 456) + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod + + $result | should -BeNullOrEmpty + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Private/Set-CertPermissions.ps1 b/Modules/Alkami.DevOps.Certificates/Private/Set-CertPermissions.ps1 new file mode 100644 index 0000000..29230bb --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Private/Set-CertPermissions.ps1 @@ -0,0 +1,42 @@ +function Set-CertPermissions { +<# +.SYNOPSIS + Assigns Certificate Permissions for a user. +#> + + [CmdletBinding()] + Param( + + [Parameter(Mandatory=$true)] + [string]$certThumprint, + + [Parameter(Mandatory=$true)] + [string]$user + ) + + $logLead = Get-LogLeadName + + $certObj = Get-ChildItem "Cert:\LocalMachine\my\$certThumprint" + $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($CertObj) + + if ($rsaCert.key -and $rsaCert.key.UniqueName) { + $fileName = $rsaCert.key.UniqueName + $directoryRsaMachineKeys = Join-Path "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\" $fileName + $directoryCryptoKeys = Join-Path "C:\ProgramData\Microsoft\Crypto\Keys\" $fileName + + if (Test-Path $directoryRsaMachineKeys) { + $path = $directoryRsaMachineKeys + } elseif (Test-Path $directoryCryptoKeys) { + $path = $directoryCryptoKeys + } else { + Write-Error "$logLead : Did not find an associated ACL File for $certThumbprint." + } + } else { + Write-Error "$logLead : Unable to determine Unique Key Name for $certThumprint" + } + + $permissions = Get-Acl -Path $path + $rule = New-Object Security.AccessControl.FileSystemAccessRule $user, "FullControl", Allow + $permissions.AddAccessRule($rule) + Set-Acl -Path $path -AclObject $permissions +} diff --git a/Modules/Alkami.DevOps.Certificates/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.Certificates/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Alkami.DevOps.Certificates/Public/Compress-Certificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Compress-Certificates.ps1 new file mode 100644 index 0000000..85254ee --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Compress-Certificates.ps1 @@ -0,0 +1,21 @@ +function Compress-Certificates { +<# +.SYNOPSIS + Combine Certificates into a .zip File. +#> + [CmdletBinding()] + param( + $Certificates, + $TempFolder + ) + #Prepare certificate folders by zipping them. + foreach ($certificate in $Certificates) { + $zipFileName = $certificate.Name.Trim() + ".zip" + $CompressedDir = $TempFolder, $zipFileName -join "\" + Remove-Item $CompressedDir -Force -ErrorAction SilentlyContinue + Remove-Item (Join-Path $certificate.Folder $zipFileName) -Force -ErrorAction SilentlyContinue + [System.IO.Compression.ZipFile]::CreateFromDirectory($certificate.Folder, + $CompressedDir, [System.IO.Compression.CompressionLevel]::Optimal, $false) + Move-Item $CompressedDir $certificate.Folder -Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.ps1 new file mode 100644 index 0000000..d6135e7 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.ps1 @@ -0,0 +1,153 @@ +function Export-Certificates { +<# +.SYNOPSIS + Exports certificates from a machine. + + .PARAMETER exportPassword + + The password used to secure the certificate with + + .PARAMETER exportPath + + The path the certificates are exported to. If no path is defined, the current working directory is used + + .PARAMETER skipRootCerts + + When this flag is supplied it will skip the exporting of certificates in the 'Root' store + + .PARAMETER skipPersonalCerts + + When this flag is supplied it will skip the exporting of certificates in the 'My' store + + .PARAMETER skipTrustedCerts + + When this flag is supplied it will skip the exporting of certificates in the 'Trusted' store + + .PARAMETER skipIACerts + + When this flag is supplied it will skip the exporting of certificates in the 'CertificateAuthority' store + +#> + + [CmdletBinding()] + Param( + [parameter(Mandatory=$false)] + [string]$exportPassword, + + [Parameter(Mandatory=$false)] + [string]$exportPath = $PWD, + + [Parameter(Mandatory=$false)] + [switch]$skipRootCerts, + + [Parameter(Mandatory=$false)] + [switch]$skipPersonalCerts, + + [Parameter(Mandatory=$false)] + [switch]$skipTrustedCerts, + + [Parameter(Mandatory=$false)] + [switch]$skipIACerts + ) + + if (!$skipPersonalCerts.IsPresent -and !$exportPassword) + { + throw "Export Password cannot be null" + } + + if ($skipRootCerts.IsPresent -and $skipPersonalCerts.IsPresent -and $skipTrustedCerts.IsPresent -and $skipIACerts.IsPresent) + { + throw "All Skip Switches cannot be set" + } + + if (!(Test-Path $exportPath)) + { + [System.IO.Directory]::CreateDirectory($exportPath) | Out-Null + } + ## Removing because of issues mocking. This shouldn't be an issue. + # Clear-Host + + [System.Reflection.Assembly]::LoadWithPartialName("System.Security.Cryptography") | Out-Null + ## TODO: Don't just blindly set the $ErrorActionPreference + $ErrorActionPreference = "Stop" + + [Collections.Generic.List[Alkami.Ops.Common.Exceptions.CertificateExportException]]$exportErrors = @() + + if (!($skipPersonalCerts.IsPresent)) + { + Write-Host "Exporting Personal Certs" + + $pfxExportPath = (Join-Path $exportPath "Personal") + + if (!(Test-Path $pfxExportPath)) + { + Write-Host "Creating directory at $pfxExportPath" + + [System.IO.Directory]::CreateDirectory($pfxExportPath) | Out-Null + } + + $errors = Export-Cert -exportPath $pfxExportPath $exportPassword -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::My) + + $exportErrors.AddRange($errors) + } + + if (!($skipIACerts.IsPresent)) + { + Write-Host "Exporting IA Certs" + + $iaExportPath = (Join-Path $exportPath "IA") + + if (!(Test-Path $iaExportPath)) + { + [System.IO.Directory]::CreateDirectory($iaExportPath) | Out-Null + } + + $errors = Export-Cert -exportPath $iaExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority) + + $exportErrors.AddRange($errors) + } + + if (!($skipRootCerts.IsPresent)) + { + Write-Host "Exporting Root Certs" + + $rootExportPath = (Join-Path $exportPath "Root") + + if (!(Test-Path $rootExportPath)) + { + [System.IO.Directory]::CreateDirectory($rootExportPath) | Out-Null + } + + $errors = Export-Cert -exportPath $rootExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::Root) + + $exportErrors.AddRange($errors) + } + + if (!($skipTrustedCerts.IsPresent)) + { + Write-Host "Exporting Trusted Certs" + + $trustedPeopleExportPath = (Join-Path $exportPath "TrustedPeople") + + if (!(Test-Path $trustedPeopleExportPath)) + { + [System.IO.Directory]::CreateDirectory($trustedPeopleExportPath) | Out-Null + } + + $errors = Export-Cert -exportPath $trustedPeopleExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople) + + $exportErrors.AddRange($errors) + } + + foreach ($exportError in $exportErrors) + { + [Alkami.Ops.Common.Exceptions.CertificateExportException]$strongError = $exportError + Write-Warning ("{0}" -f $strongError.Message) + Write-Warning ("`tError: {0}" -f $strongError.BaseExceptionMessage.TrimEnd()) + Write-Warning ("`tName: {0}" -f $strongError.CertificateName) + Write-Warning ("`tThumbprint: {0}" -f $strongError.CertificateThumbPrint) + Write-Warning ("`tSubject: {0}" -f $strongError.Subject.Trim()) + Write-Output `n + } +} + diff --git a/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.tests.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.tests.ps1 new file mode 100644 index 0000000..408c5b8 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Export-Certificates.tests.ps1 @@ -0,0 +1,128 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +# Yes. I hate this at least as much as you do. If you can find a way around it, please show me. +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$compiledModuleForMock = "Alkami.DevOps.Certificates" +$exportPassword = "Test" +$exportPath = "c:\temp\CertificateTest" + +Remove-FileSystemItem -Path $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null +New-Item -ItemType Directory $exportPath -Force | Out-Null + +InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $inScopeModuleForAssert = "Alkami.DevOps.Certificates" + $exportPassword = "Test" + $exportPath = "c:\temp\CertificateTest" + $moduleForMock = "" + + Mock -CommandName Export-Cert { + [Collections.Generic.List[Alkami.Ops.Common.Exceptions.CertificateExportException]] $emptyArr = @() + return ,$emptyArr + } -ModuleName $moduleForMock + #prevents the mehtod under test from clearing the screen during testing. + Mock -ModuleName $moduleForMock -CommandName Clear-Host {} + Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock + + Describe "Export-Certificates" { + + Context "When there are bad inputs when calling Export-Certificates" { + + It "Throws Exception if all skip flags set" { + + { Export-Certificates $exportPassword -skipPersonalCert -skipRootCerts -skipTrustedCert -skipIACert } | Should Throw + } + } + + Context "When the parameters are valid" { + + It "Doesnt Require Password if not exporting Personal Certificates" { + { Export-Certificates -skipPersonalCerts } | Should -Not -Throw + } + + It "Creates Path if it doesn't exist" { + + $path = 'c:\temp\badPath' + + Export-Certificates $exportPassword -exportPath $path + + $path | Should -Exist + } + + It "Creates Personal Directory" { + + Export-Certificates $exportPassword -exportPath $exportPath + + Join-Path $exportPath "Personal" | Should -Exist + } + + It "Creates IA Directory" { + + Export-Certificates $exportPassword -exportPath $exportPath + + Join-Path $exportPath "IA" | Should -Exist + } + + It "Creates Root Directory" { + + Export-Certificates $exportPassword -exportPath $exportPath + + Join-Path $exportPath "Root" | Should -Exist + } + + It "Creates TrustedPeople Directory" { + + Export-Certificates $exportPassword -exportPath $exportPath + + Join-Path $exportPath "TrustedPeople" | Should -Exist + } + + It "Calls Export-Certs 4 times when no filters are used" { + + + Export-Certificates $exportPassword -exportPath $exportPath + + #$result | Should be {} + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 4 -Exactly -Scope It + } + + It "Calls Export-Certs 3 times when skipRootCerts filter used" { + + Export-Certificates $exportPassword -exportPath $exportPath -skipRootCerts + + #$result | Should be {} + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It + } + + It "Calls Export-Certs 3 times when skipPersonalCerts filter used" { + + Export-Certificates $exportPassword -exportPath $exportPath -skipPersonalCerts + + #$result | Should be {} + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It + } + + It "Calls Export-Certs 3 times when skipTrustedCerts filter used" { + + Export-Certificates $exportPassword -exportPath $exportPath -skipTrustedCerts + + #$result | Should be {} + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It + } + + It "Calls Export-Certs 3 times when skipIACerts filter used" { + + Export-Certificates $exportPassword -exportPath $exportPath -skipIACerts + + #$result | Should be {} + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It + } + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Export-CertificatesToFileSystem.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Export-CertificatesToFileSystem.ps1 new file mode 100644 index 0000000..9076a95 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Export-CertificatesToFileSystem.ps1 @@ -0,0 +1,167 @@ +function Export-CertificatesToFileSystem { +<# +.SYNOPSIS +Exports certificates from desired stores (My, CertificateAuthority, Root, and TrustedPeople by default) +onto the file system. + +.DESCRIPTION +This script will export all public certificates and private keys in a given store to the file system. The +certificate chain is also exported in an individualized folder along with the certificate so that you +can delineate what certificates are needed for the one exported. + +A password is generated for private keys, if ADGroups are given they are assigned to the private keys as well. +This script also returns a model of the data exported. With this model the file system data is more +easily manipulated and was originally intended to be used by a module for uploading these certificates +and their chains to individual secrets in secret server. + +.PARAMETER PodName +String +Used to create a leaf folder with in which the certificates will be exported and set as a property +on the model returned as a result of this cmdlet. + +.PARAMETER CertRoot +String +Location that this cmdlet will be working out of. This is set to LocalMachine but could be set to Personal +or other certificate providers. + +.PARAMETER StoresToExport +StoreName[] +All certificates in these stores will be exported along with their certificate chains and private keys. + +.PARAMETER ADGroups +String[] +Any ADGroups or UserAccounts that are given will be assigned to private keys exported. These users +will be able to import the private keys without a password, although one is generated and set +by this cmdlet regardless + +.EXAMPLE +Export-CertificatesToFileSystem -PodName "Pod 1.1.1" + +Base usage, will export all certificates in the default stores and their private keys to the Default Export Root +Folder under leaf folder 'Pod 1.1.1'. A password will be generated for the private keys and returned to the +caller in the model object. + +.EXAMPLE +Export-CertificatesToFileSystem -PodName "Pod 7.0.5" -ADGroups "corp\JSmith","corp\CCarter" -RootExportFolder "C:\Exports" + +Will export all certificates in the default stores and their private keys to the C:\Exports +Folder under leaf folder 'Pod 7.0.5'. Users JSmith and CCarter will be able to import the private keys +without the password, however passwords generated for these certificates will be returned with the +model object that this cmdlet produces. + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [string]$PodName, + [Parameter(Mandatory = $False)] + [string]$CertRoot = "Cert:\LocalMachine\", + [Parameter(Mandatory = $False)] + [string]$RootExportFolder = "c:\temp\CertExports", + [Parameter(Mandatory = $False)] + $StoresToExport = @([System.Security.Cryptography.X509Certificates.StoreName]::My, + [System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority, + [System.Security.Cryptography.X509Certificates.StoreName]::Root, + [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople), + [Parameter(Mandatory = $False)] + [string[]]$ADGroups = "" + ) + + begin { + + $logLead = Get-LogLeadName + + $Pod = [PSObject]@{ + PodName = $PodName + ExportFolder = (Join-Path $RootExportFolder $PodName) + StoresToExport = $StoresToExport + CertRoot = $CertRoot + Stores = @{ } + } + + if (Test-Path -Path $Pod.ExportFolder) { + Write-Host "$logLead : Removing items from $($Pod.ExportFolder) before beginning" + Remove-FileSystemItem -Path $Pod.ExportFolder -Recurse -Force -ErrorAction SilentlyContinue | Out-Null + } + + if (-not (Test-Path -PathType Container -Path $Pod.ExportFolder)) { + New-Item $Pod.ExportFolder -ItemType Directory | Out-Null + } + } + + process { + foreach ($storeName in $Pod.StoresToExport) { + $store = [PSObject]@{ + Name = $storeName + ExportStorePath = Join-Path $Pod.ExportFolder $storeName + StorePath = Join-Path $Pod.CertRoot $storeName + Certificates = @{ } + } + + # Powershell's Paths don't match the X509 enum. So here we do a poor man's lookup. If we ever need more stores than just the CA, this needs to be expanded to something more robust and clean. + if ($store.Name -eq "CertificateAuthority") { + $store.StorePath = "Cert:\LocalMachine\CA" + } + + $Pod.Stores.Add($storeName, $store) + $Pod.Stores | Add-Member -MemberType NoteProperty -Name $storeName -Value $store + + New-Item $store.ExportStorePath -ItemType Directory | Out-Null + + #foreach cert in store + try { + $certificates = Get-ChildItem $store.StorePath; + } + catch { + Write-Warning "Cannot find Certificates in $storeName" + } + if ($certificates.Count -gt 0) { + foreach ($cert in $certificates) { + + try { + + $exportInfo = Export-CertificateToFileSystem -cert $cert -exportStorePath $store.ExportStorePath -ADGroups $ADGroups + if ($null -eq $exportInfo) { continue } + + $chainInfo = Export-CertChain -cert $cert -exportStorePath $store.ExportStorePath -exportCertPath $exportInfo.exportCertPath -ADGroups $ADGroups + + $CertificateChain = [PSObject]@{ + Folder = Join-Path $exportInfo.ExportCertPath "ChainedCertificates" + Certificates = @{ } + } + + foreach ($chainedCert in $chainInfo) { + $CertificateChain.Certificates.Add($chainedCert.certName, $chainedCert) + $CertificateChain.Certificates | Add-Member -MemberType NoteProperty -Name $chainedCert.certName -Value $chainedCert + } + + $certificate = [PSObject]@{ + Name = $exportInfo.certName + FilePath = $exportInfo.exportCertFile + Folder = $exportInfo.exportCertPath + Password = $exportInfo.certPassword + CertificateChain = $CertificateChain + ADGroups = $exportInfo.ADGroups + ExpirationDate = $exportInfo.ExpirationDate + Thumbprint = $exportInfo.Thumbprint + } + + $store.Certificates.Add($certificate.Name, $certificate) + + $store.Certificates | Add-Member -MemberType NoteProperty -Name $certificate.Name -Value $certificate + } + catch { + + Write-Error "Certificate has failed to export + Name: $($certificate.Name), FriendlyName: $($cert.FriendlyName), Thumprint: $($cert.Thumbprint), Store: $storeName + $($_.Exception) $($_.Message) $($_.ScriptStackTrace)" + + } + } + } + } + } + end { + return $Pod + } +} diff --git a/Modules/Alkami.DevOps.Certificates/Public/Get-ExpiringCertificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Get-ExpiringCertificates.ps1 new file mode 100644 index 0000000..64eb79c --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Get-ExpiringCertificates.ps1 @@ -0,0 +1,69 @@ +function Get-ExpiringCertificates { +<# +.SYNOPSIS +Gets certificates that will expire soon. + +.DESCRIPTION +Takes a list of machines and connects to their certificate stores, compares the expiration date +to a configureable threshold date. If the expiration date is less than the threshold date the +certificate is returned in a list. + +.PARAMETER ComputerName +[string[]]One or more computers on which to get expired certificates from. + +.PARAMETER ExpirationThreshold +[int] An amount of days you wish to set the threshold. +Note* Can be negative. Defaults to 30 + +.EXAMPLE +Get-ExpiringCertificates "Server1","Server2" +Will connect to these servers in parallel, and retrieve certificates that are due to expire within 30 days or less from now. + + +.EXAMPLE +Get-ExpiringCertificates "Server1","Server2" -Threshold 90 +Will connect to these servers in parallel, and retrieve certificates that are due to expire within 90 days or less from now. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [Alias("Servers","Machines")] + [string[]]$ComputerName, + [Parameter(Mandatory=$false)] + [int]$ExpirationThreshold = 30 + ) + + begin{ + #Ensure there are machines to connect to + $sessions = New-PSSession $ComputerName -ErrorAction SilentlyContinue; + $Unreachable = $ComputerName | Where-Object {$sessions.ComputerName -notcontains $_} + if($Unreachable){Write-Host "Could not connect to $Unreachable";} + if(!$sessions){throw "Could not connect to any machines";} + } + process{ + $ScriptBlock = { + param($ExpirationThreshold); + + $personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My; + $machineStore = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine; + + $certificates = [Alkami.Ops.Common.Cryptography.CertificateHelper]::GetAllCertificates($personalStore, $machineStore, $env:COMPUTERNAME); + + $expirationThresholdDate = (Get-Date).AddDays($ExpirationThreshold); + + #Filter certificates by threshold date + $expiredCertificates = $certificates | Where-Object {$_.notAfter -lt $expirationThresholdDate} | ` + Select-Object @{N="Machine";E={$env:COMPUTERNAME}},@{N="ExpirationDate";E={$_.NotAfter}},` + @{N="DaysRemaining";E={(New-TimeSpan -start (get-date) -end $_.notAfter | Select-Object -ExpandProperty days)}},Thumbprint,FriendlyName,Subject; + + if($expiredCertificates){Write-Output $expiredCertificates;} + } + + #Connect to machines and execute + $expiredCertificates = Invoke-Command -Session $sessions -ScriptBlock $ScriptBlock -ArgumentList $ExpirationThreshold; + + Remove-PSSession $sessions; + + return $expiredCertificates; + } +} diff --git a/Modules/Alkami.DevOps.Certificates/Public/Get-PrivateKeyPermissions.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Get-PrivateKeyPermissions.ps1 new file mode 100644 index 0000000..990b614 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Get-PrivateKeyPermissions.ps1 @@ -0,0 +1,37 @@ +function Get-PrivateKeyPermissions { +<# +.SYNOPSIS + Fetch Private Key Permissions. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate + ) + + $logLead = (Get-LogLeadName); + + try { + + $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate) + } + catch { + + Write-Warning "$logLead : Exception Occurred While Reading Private Key Details for Certificate with Thumbprint $($certificate.Thumbprint): $($_.Exception.Message.Trim())" + return $null + } + + $rsaKeyFileName = $rsaKey.Key.UniqueName + $rsaKeyPath = "${env:ALLUSERSPROFILE}\Microsoft\Crypto\RSA\MachineKeys\$rsaKeyFileName" + + if (Test-Path $rsaKeyPath) { + + return (Get-Acl -Path $rsaKeyPath).Access + } + else { + + Write-Warning "$logLead : Unable to Find Private Key for Certificate with Thumbprint $($certificate.Thumbprint) at $rsaKeyPath" + return $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Get-SecretServerConnection.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Get-SecretServerConnection.ps1 new file mode 100644 index 0000000..590ddf6 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Get-SecretServerConnection.ps1 @@ -0,0 +1,26 @@ +function Get-SecretServerConnection { +<# +.SYNOPSIS + Exports a [SecretServerConnection] which can be used from Powershell's commandline. Largely for testing purposes. + +.PARAMETER Site + The uri for the Secret Server to connect to. + +.PARAMETER userName + Secret Server username. + +.PARAMETER password + Secret Server password. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Site, + [Parameter(Mandatory = $true)] + [string]$UserName, + [Parameter(Mandatory = $true)] + [string]$Password + ) + + return [SecretServerConnection]::new([string]$Site, [string]$UserName, [string]$Password); +} diff --git a/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.ps1 new file mode 100644 index 0000000..0e2d576 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.ps1 @@ -0,0 +1,194 @@ +function Import-Certificates { + <# +.SYNOPSIS + Imports certificates onto a machine. + + .PARAMETER importPassword + + The password used to import the certificate with. Only used when Personal store certificates are imported. + + .PARAMETER importPath + The path the certificates are imported from. If no path is defined, the current working directory is used. + + .PARAMETER usersWhoNeedRights + Optional list of users to grant rights for certificates which have private keys. If not supplied, defaults to IIS_IUSRS + $defaultUsersWhoNeedRights array. + + .PARAMETER securityGroup + The Security Group for each GMSA account (pod6, pod8). When not supplied, defaults to the value in the machine.config on the current server. + If not found in machine.config, throws. + + .PARAMETER skipRootCerts + + When this flag is supplied it will skip the importing of certificates in the 'Root' store. + + .PARAMETER skipPersonalCerts + + When this flag is supplied it will skip the importing of certificates in the 'My' store. + + .PARAMETER skipTrustedCerts + + When this flag is supplied it will skip the importing of certificates in the 'Trusted' store. + + .PARAMETER skipIACerts + + When this flag is supplied it will skip the importing of certificates in the 'CertificateAuthority' store. + +#> + [CmdletBinding()] + Param( + [parameter(Mandatory = $false)] + [string]$importPassword, + + [Parameter(Mandatory = $false)] + [string]$importPath = $PWD, + + [Parameter(Mandatory = $false)] + [string[]]$usersWhoNeedRights = @("IIS_IUSRS"), + + [Parameter(Mandatory = $false)] + [string]$securityGroup, + + [Parameter(Mandatory = $false)] + [hashtable]$certPasswordList = @{ }, + + [Parameter(Mandatory = $false)] + [switch]$skipRootCerts, + + [Parameter(Mandatory = $false)] + [switch]$skipPersonalCerts, + + [Parameter(Mandatory = $false)] + [switch]$skipTrustedCerts, + + [Parameter(Mandatory = $false)] + [switch]$skipIACerts + ) + + $unsignedCertFileIncludeFilter = @("*.cer", "*.crt") + + # Modify this to add/remove accounts as defaults when usersWhoNeedRights is not supplied by the user + # Will be formatted as FH\. + $defaultUsersWhoNeedRights = @("radium$", "nag$", "dbms$", "micro$") + + if (!$skipPersonalCerts.IsPresent -and !$importPassword) { + throw "Import Password cannot be null"; + } + + if ($skipRootCerts.IsPresent -and $skipPersonalCerts.IsPresent -and $skipTrustedCerts.IsPresent -and $skipIACerts.IsPresent) { + throw "All Skip Switches cannot be set"; + } + + if (!(Test-Path $importPath)) { + throw "Import Path not found"; + } + + if ( !($PSBoundParameters.ContainsKey('securityGroup')) ) { + Write-Host "securityGroup was not supplied by the user. Attempting to pull securityGroup from Environment.UserPrefix in machine.config." + + if ($securityGroup = Get-AppSetting Environment.UserPrefix) { + Write-Host "securityGroup read from machine.config as: $securityGroup" + } else { + throw("securityGroup was not supplied by the user and could not be found in the machine config. Please specify the security group (i.e. pod6, pod8) and rerun this function.") + } + } + + if ( !($PSBoundParameters.ContainsKey('usersWhoNeedRights')) ) { + Write-Host "usersWhoNeedRights was not supplied by the user. Using default accounts." + + $defaultUsersWhoNeedRights = foreach ($defaultUserWhoNeedsRights in $defaultUsersWhoNeedRights) { + "fh\$securityGroup.$defaultUserWhoNeedsRights" + } + + $usersWhoNeedRights += $defaultUsersWhoNeedRights + } + + $rootImportPath = (Join-Path $importPath "Root"); + + if (!($skipRootCerts.IsPresent) -and (Test-Path $rootImportPath)) { + Write-Host "Importing Root Certs"; + + $certs = Get-ChildItem $rootImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*"; + + foreach ($cert in $certs) { + Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::Root); + } + } + + $iaImportPath = (Join-Path $importPath "IA"); + + if (!($skipIACerts.IsPresent) -and (Test-Path $iaImportPath)) { + Write-Host "Importing IA Certs"; + + $certs = Get-ChildItem $iaImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*"; + + foreach ($cert in $certs) { + Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority); + } + } + + $trustedImportPath = (Join-Path $importPath "TrustedPeople"); + + if (!($skipTrustedCerts.IsPresent) -and (Test-Path $trustedImportPath)) { + Write-Host "Importing Trusted Certs"; + + $certs = Get-ChildItem $trustedImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*"; + + foreach ($cert in $certs) { + Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople); + } + } + + $pfxImportPath = (Join-Path $importPath "Personal"); + + if (!($skipPersonalCerts.IsPresent) -and (Test-Path $pfxImportPath)) { + Write-Host "Importing Personal Certs"; + + $certs = Get-ChildItem $pfxImportPath -Recurse -include @("*.pfx", "*.cer") -Exclude "WMSVC*"; + + # Add any additional Service Users to $usersWhoNeedRights if they're assigned to a Windows Service + # and are running under the environment's gMSA Security Group + $ServiceUserAccounts = Get-CIMInstance Win32_Service | Where-Object { $_.StartName -match $securityGroup } + if ($ServiceUserAccounts) { + $usersWhoNeedRights += $ServiceUserAccounts.StartName + } + + $usersWhoNeedRights = $usersWhoNeedRights | Sort-Object | Get-Unique + + foreach ($cert in $certs) { + if ($cert.Extension -eq ".pfx") { + $currentCertPassword = $importPassword; + + if ($certPasswordList.ContainsKey($cert.Name)) { + $currentCertPassword = certPasswordList.Get_Item($cert.Name); + } + + Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::My) $currentCertPassword; + + Confirm-Cert $cert.Name ([System.Security.Cryptography.X509Certificates.StoreName]::My); + + $x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert.FullName, $currentCertPassword); + + $usersWhoNeedRights | ForEach-Object { + + $user = $_; + + Write-Host ("Granting {0} rights to PK for certificate {1}" -f $user, $targetCert.Name); + + Set-CertPermissions $x509cert.Thumbprint $user; + } + } else { + $currentCertPassword = $importPassword; + + if ($certPasswordList.ContainsKey($cert.Name)) { + $currentCertPassword = certPasswordList.Get_Item($cert.Name); + } + + Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::My); + + Confirm-Cert $cert.Name ([System.Security.Cryptography.X509Certificates.StoreName]::My); + + + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.tests.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.tests.ps1 new file mode 100644 index 0000000..1f412e9 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Import-Certificates.tests.ps1 @@ -0,0 +1,130 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$exportPassword = "Test" +$exportPath = "c:\temp\CertificateTest" +$usersWhoNeedRights = @("testuser1", "testuser2") + +Remove-FileSystemItem -Path $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null +New-Item -ItemType Directory $exportPath -Force | Out-Null + +InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $inScopeModuleForAssert = "Alkami.DevOps.Certificates" + $moduleForMock = "" + $exportPassword = "Test" + $exportPath = "c:\temp\CertificateTest" + $usersWhoNeedRights = @("testuser1", "testuser2") + + + Describe "Import-Certificates" { + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null } + + Context "When there are bad inputs when calling Import-Certificates" { + + It "Throws Exception if all skip flags set" { + + { Import-Certificates $exportPassword -skipPersonalCert -skipRootCerts -skipTrustedCert -skipIACert -securityGroup "pod1" } | Should -Throw + } + + It "Throws Exception if path doesn't exist" { + + { Import-Certificates $exportPassword -importPath 'C:\BadPath' -securityGroup "pod1" } | Should -Throw + } + + It "Throws Exception if securityGroup is not supplied and it is not found in machine.config" { + + { Import-Certificates $exportPassword } | Should -Throw + } + } + + Context "When Inputs are correct" { + + Mock -ModuleName $moduleForMock Join-Path { return "C:\temp\testpath" } + Mock -ModuleName $moduleForMock Test-Path { return $true } + Mock -ModuleName $moduleForMock -CommandName Import-Cert { } + Mock -ModuleName $moduleForMock Confirm-Cert { } -Verifiable + Mock -ModuleName $moduleForMock Get-ChildItem { return @{ FullName = "c:\temp\testpath\Test.pfx"; Name = "Test.pfx"; Extension = ".pfx"} } + Mock -ModuleName $moduleForMock Get-AlkamiServices { @{ Name = "Alkami.Radium"} } + Mock -ModuleName $moduleForMock Get-CIMInstance { @{ StartName = "podtest.user"} } + Mock -ModuleName $moduleForMock Set-CertPermissions {} + Mock -ModuleName $moduleForMock New-Object { @{ Thumbprint = "ABCDEFG"} } + + It "Doesnt Require Password if not exporting Personal Certificates" { + + { Import-Certificates -skipPersonalCerts -securityGroup "pod1" } | Should -Not -Throw + } + + It "Calls Import-Cert when importing personal certs" { + + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It + } + + It "Calls Confirm-Cert when importing personal certs" { + + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Confirm-Cert -Times 1 -Exactly -Scope It + } + + It "Calls Set-CertPermissions for default users + test WMI user when usersWhoNeedRights is not supplied" { + + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 5 -Exactly -Scope It + } + + It "Calls Set-CertPermissions for supplied users + test WMI user when usersWhoNeedRights is supplied" { + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -usersWhoNeedRights $usersWhoNeedRights -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 2 -Exactly -Scope It + } + + It "Calls Set-CertPermissions for default users + test WMI user when usersWhoNeedRights is not supplied and additional users found in services" { + Mock -ModuleName $moduleForMock Get-CIMInstance { @( @{StartName = "pod1.user"}, @{StartName = "podtest.user"} ) } + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 6 -Exactly -Scope It + } + + It "Calls Set-CertPermissions for supplied users + test WMI user when usersWhoNeedRights is supplied and additional users found in services" { + Mock -ModuleName $moduleForMock Get-CIMInstance { @( @{StartName = "pod1.user"}, @{StartName = "podtest.user"} ) } + Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -usersWhoNeedRights $usersWhoNeedRights -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 3 -Exactly -Scope It + } + + It "Calls Import-Cert when importing root certs" { + + Import-Certificates $exportPassword -skipPersonalCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It + } + + It "Calls Import-Cert when importing trusted certs" { + + Import-Certificates $exportPassword -skipPersonalCerts -skipRootCerts -skipIACerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It + } + + It "Calls Import-Cert when importing IA certs" { + + Import-Certificates $exportPassword -skipPersonalCerts -skipRootCerts -skipTrustedCerts -securityGroup "pod1" + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.ps1 new file mode 100644 index 0000000..4baa547 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.ps1 @@ -0,0 +1,98 @@ +function Import-PfxCertificateWithPermissions { +<# +.SYNOPSIS + Import a PFX certificate with appropriate user permissions without needing the folder structure as specified in Import-Certificates. + + .PARAMETER ImportPassword + The password used to import the certificate. + + .PARAMETER PathToPfxCertificate + The path to a specified PFX file. + + .PARAMETER UsersWhoNeedRights + Optional list of users to grant rights for certificates which have private keys. If not supplied, then the resulting Import-Certificates call will assign the default users + + .EXAMPLE + + Import-PfxCertificateWithPermissions -ImportPassword 'PASSWORD_GOES_HERE' -PathToPfxCertificate "\\10.0.16.67\c`$\temp\mccu.com.pfx" + [Import-PfxCertificateWithPermissions] : Copied cert to C:\Users\ccoane\AppData\Local\Temp\2\904bf9e6-4be5-4b8e-805b-03817b6dd198\Personal + Importing Personal Certs + Validating certificate mccu.com.pfx + Certificate mccu.com.pfx Passed Validation + Granting fh\dev.dbms$ rights to PK for certificate + Granting fh\dev.micro$ rights to PK for certificate + Granting FH\dev.micro$ rights to PK for certificate + Granting fh\dev.nag$ rights to PK for certificate + Granting fh\dev.radium$ rights to PK for certificate + Granting iis_iusrs rights to PK for certificate + [Import-PfxCertificateWithPermissions] : Removed temporary directory C:\Users\ccoane\AppData\Local\Temp\2\904bf9e6-4be5-4b8e-805b-03817b6dd198 + + + // If you need to make a modification to the default user list used by Import-Certificates e.g. PFX also requires access granted to fh\xxxx.bank$ + $SecurityGroup = Get-AppSetting -appSettingKey "Environment.UserPrefix" + Import-PfxCertificateWithPermissions -ImportPassword 'PASSWORD_GOES_HERE' -PathToPfxCertificate "\\10.0.16.67\c`$\temp\mccu.com.pfx" -UsersWhoNeedRights @("iis_iusrs", "fh\$SecurityGroup.radium$", "fh\$SecurityGroup.nag$", "fh\$SecurityGroup.dbms$", "fh\$SecurityGroup.micro$", "fh\$SecurityGroup.bank$") + [Import-PfxCertificateWithPermissions] : Copied cert to C:\Users\ccoane\AppData\Local\Temp\2\a7ad2893-884b-4604-bed2-2c2d6bd597da\Personal + Importing Personal Certs + Validating certificate mccu.com.pfx + Certificate mccu.com.pfx Passed Validation + Granting fh\dev.bank$ rights to PK for certificate + Granting fh\dev.dbms$ rights to PK for certificate + Granting FH\dev.micro$ rights to PK for certificate + Granting fh\dev.micro$ rights to PK for certificate + Granting fh\dev.nag$ rights to PK for certificate + Granting fh\dev.radium$ rights to PK for certificate + Granting iis_iusrs rights to PK for certificate + [Import-PfxCertificateWithPermissions] : Removed temporary directory C:\Users\ccoane\AppData\Local\Temp\2\a7ad2893-884b-4604-bed2-2c2d6bd597da + +#> + [CmdletBinding()] + Param( + [parameter(Mandatory=$true)] + [string]$ImportPassword, + + [Parameter(Mandatory=$true)] + [string]$PathToPfxCertificate, + + [Parameter(Mandatory=$false)] + [string[]]$UsersWhoNeedRights + ) + + $logLead = (Get-LogLeadName) + + # Verify Pfx is valid and exists/accessible + if (!($PathToPfxCertificate -Like "*.pfx")) { + Write-Error "$logLead : This function is expecting a certificate with a .pfx extension as the value for `$PathToPfxCertificate. Provided value is: $PathToPfxCertificate" + return + } + + if (!(Test-Path $PathToPfxCertificate)) { + Write-Error "$logLead : Unable to reach the specified file from this server. Verify the path is correct and accessible to this server. Provided value is: $PathToPfxCertificate" + return + } + + try { + $randomPath = Join-Path $Env:Temp $(New-Guid) + + # Copy PFX to a randomly created folder in appropriate Import-Certificates folder structure + $tempFolderPersonalPath = New-Item -Path $randomPath -ItemType Directory -Name "Personal" -Force + Copy-Item -Path $PathToPfxCertificate -Destination $tempFolderPersonalPath + Write-Host "$logLead : Copied cert to $tempFolderPersonalPath" + + # If user provided an argument for $UsersWhoNeedRights + if ($UsersWhoNeedRights) { + Import-Certificates -importPassword $ImportPassword -importPath $randomPath -usersWhoNeedRights $UsersWhoNeedRights + } else { + Import-Certificates -importPassword $ImportPassword -importPath $randomPath + } + } catch { + Write-Error "$logLead : $_" + } finally { + # Delete the randomly created folder if it exists + if (Test-Path -Path $randomPath) { + Remove-Item -Path $randomPath -Recurse -Force + Write-Host "$logLead : Removed temporary directory $randomPath" + } + } + + return $randomPath +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.tests.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.tests.ps1 new file mode 100644 index 0000000..2a6170f --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Import-PfxCertificateWithPermissions.tests.ps1 @@ -0,0 +1,52 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 + +$importPasswordTest = "PASSWORD_GOES_HERE" +$pathToCertificateIsADirectory = "C:\Temp\Personal" +$pathToCertificateIsNotAPfxFile = "C:\Temp\Personal\Certificate.crt" +$pathToCertificateIsNotAccessible = "C:\Temp\Personal\CertificateThatShouldNotBeAccessibleUnlessYouMakeThisFileToSpiteMe.pfx" +$pathToCertificatePfxFile = "C:\Temp\Personal\Certificate.pfx" + +$WriteErrorIncorrectPfxArgument = "This function is expecting a certificate with a .pfx extension as the value for" +$WriteErrorInaccessiblePfxFile = "Unable to reach the specified file from this server" + +Describe "Import-PfxCertificateWithPermissions" { + + Mock -CommandName Copy-Item -ModuleName Alkami.DevOps.Certificates -MockWith { } + Mock -CommandName Import-Certificates -ModuleName Alkami.DevOps.Certificates -MockWith { } + Mock -CommandName Write-Error -ModuleName Alkami.DevOps.Certificates -MockWith { } + + Context "When there are bad inputs when calling Import-PfxCertificateWithPermissions" { + + Mock -CommandName Test-Path -ModuleName Alkami.DevOps.Certificates -MockWith { return $false } + + It "Writes Error if path argument has a folder path and does not point to a PFX file" { + Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsADirectory + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorIncorrectPfxArgument } + } + + It "Writes Error if path argument has a file path but does not point to a PFX file" { + Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsNotAPfxFile + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorIncorrectPfxArgument } + } + + It "Writes Error if path argument does not point to an accessible PFX file" { + Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsNotAccessible + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorInaccessiblePfxFile } + } + } + + Context "When Inputs are correct" { + + Mock -CommandName Test-Path -ModuleName Alkami.DevOps.Certificates -MockWith { return $true } + + It "Assert reaches Import-Certificates " { + Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificatePfxFile + Assert-MockCalled -ModuleName Alkami.DevOps.Certificates Import-Certificates -Times 1 -Exactly -Scope It + } + + It "Assert randomly created folder doesn't exist after function completion " { + $folder = Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificatePfxFile + $folder | Should -Not -Exist + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Import-PodFromSecretServer.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Import-PodFromSecretServer.ps1 new file mode 100644 index 0000000..cd5a1f5 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Import-PodFromSecretServer.ps1 @@ -0,0 +1,184 @@ +function Import-PodFromSecretServer { +<# +.SYNOPSIS + Import Pod Certificates from Secret Server. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + $PodName, + [Parameter(Mandatory = $false, ParameterSetName = "credentials")] + [string]$SecretServerUrl ="https://alkami.secretservercloud.com", + [Parameter(Mandatory = $false, ParameterSetName = "credentials")] + $SecretServerUserName, + [Parameter(Mandatory = $false, ParameterSetName = "credentials")] + $SecretServerPassword, + [Parameter(Mandatory = $false)] + [ValidateSet("Web", "App")] + $ServerType, + [Parameter(Mandatory = $false, HelpMessage = "ADGroups that need permission to these certificates")] + [string[]]$ADGroups, + [Parameter(Mandatory = $false)] + $TempDirectory = "c:\temp\importTempDir", + [Parameter(Mandatory = $false)] + [string]$CertRoot = "Cert:\LocalMachine\", + [Parameter(Mandatory = $false)] + [switch]$UsePassword, + [Parameter(Mandatory = $false)] + [ValidateSet("Production", "Staging")] + [string]$EnvironmentType + ) + begin { + Import-AWSModule # SSM + + New-Item -Path $TempDirectory -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null + + switch ($EnvironmentType) { + "Production" {$RootFolderName = "Production-CertApi"} + "Staging" {$RootFolderName = "Staging-CertApi"} + } + + # Get all parmeters which weren't supplied. + if(!$EnvironmentType) + { + $EnvironmentType = Get-AppSetting Environment.Type + } + + if(!$PodName) + { + $PodName = Get-AppSetting Environment.Name + } + + if(!$ServerType) + { + $ServerType = Get-AppSetting Environment.Server + } + + if((!$SecretServerUserName -or !$SecretServerPassword)) + { + if(Test-IsAws) + { + if(!$SecretServerUserName) { + $SecretServerUserName = (Get-SSMParameter -Name "secret_server_api_username" -WithDecryption $true).Value + } + + if(!$SecretServerPassword) { + $SecretServerPassword = (Get-SSMParameter -Name "secret_server_api_password" -WithDecryption $true).Value + } + } + else { + Write-Error "Secret credentials must be manually provided if this is run on a non-AWS machine. They cannot be retrieved from SSM." + } + } + + $secretServer = [SecretServerConnection]::new($SecretServerUrl, $SecretServerUserName, $SecretServerPassword) + $UseTwoFactor = $false + $secretServer.Authenticate($UseTwoFactor) + + $NameOfCertificateField = "pfx-file" + $NameOfPasswordField = "Import Password" + Add-Type -Assembly System.IO.Compression.FileSystem + } + process { + $startTime = Get-Date + Write-Debug "Getting folder info" + $environmentFolderId = $secretServer.GetFolderIdByName($RootFolderName) + $podFolderId = $secretServer.GetFolderIdByName($PodName, $environmentFolderId) + $subFolderId = $secretServer.GetFolderIdByName($ServerType, $podFolderId) # Should be at an App/Mic/Web folder by this point. These folder names aren't unique, hence getting the environment folder id above. + + $storeFolders = $secretServer.GetChildFolders($subFolderId) + + $timeElapsed = New-TimeSpan -Start $startTime -End (Get-Date) + Write-Debug "Folder info retreived - $timeElapsed" + + foreach ($storeFolder in $storeFolders) { + if ($storeFolder.FolderName -eq "CertificateAuthority") {$sanitizedStore = "CA"} else {$sanitizedStore = $storeFolder.FolderName} # Powershell's Paths don't match the X509 enum. So here we do a poor man's lookup. If we ever need more stores than just the CA, this needs to be expanded to something more robust and clean. + + $TempStoreDirectory = Join-Path $TempDirectory $storeFolder.FolderName + + $installedCerts = (Get-Cert -StoreName $sanitizedStore).thumbprint # Get certs in the current store. + + Write-Debug "Getting Secrets and downloading certificate chain" + $secretInfoList = $secretServer.GetSecretByFolderId($storeFolder.id) + $secretInfoList | Where-Object {$installedCerts -notcontains $_.name.split("-")[-1]} + foreach ($secretInfo in $secretInfoList) { + $secret = $secretServer.GetSecretById($secretInfo.id) + + # Don't import expired certs + $certExpirationDate = $secret.items | Where-Object {$_.fieldName -eq "ExperationDate"} + if ((Get-Date $certExpirationDate.itemValue) -gt (Get-Date)) { + if ($installedCerts) { #Don't die if there are no certs + $secretThumbprint = $secret.items | Where-Object {$_.fieldName -eq "Thumbprint"} | Select-Object -ExpandProperty itemValue + if ($installedCerts.Contains($secretThumbprint)) { + Write-Verbose "Certificate with thumbprint $secretThumbprint already installed" + continue + } + } + if ($UsePassword) { + $TempPassword = $secret.items | Where-Object {$_.fieldName -eq $NameOfPasswordField} | Select-Object -ExpandProperty itemValue + $Password = ConvertTo-SecureString $TempPassword -AsPlainText -Force + } + + $folder = Join-Path $TempStoreDirectory $secret.Name + Remove-Item $folder -Force -ea SilentlyContinue -Recurse + New-Item $folder -ItemType Directory -Force -ea SilentlyContinue | Out-Null + + $zipFolder = $folder, "$($secret.Name).zip" -join "\" + if ($secretServer.DownloadFile($secret.Id, $NameOfCertificateField, $zipFolder)) { + $timeElapsed = New-TimeSpan -Start $startTime -End (Get-Date) + Write-Debug "downloaded... - $timeElapsed" + + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipFolder, $folder) + Write-Debug "unzipping complete - $timeElapsed" + + $certificateFiles = Get-ChildItem $folder -Include "*.cer", "*.pfx" -Recurse + $certList = [System.Collections.Arraylist]::new() + foreach ($file in $certificateFiles) { + $certStore = if ($file.Directory.Name -Match $file.BaseName) {$sanitizedStore}else {$file.Directory.Name} + + [void]$certList.Add(([PSObject]@{ + file = $file + certStore = Join-path $CertRoot $certStore + })) + } + + foreach ($cert in $certList) { + if ($cert.file.extension -eq ".cer") { + try { + Import-Certificate -FilePath $cert.file.FullName -CertStoreLocation $cert.certStore | Out-null + } + catch { + Write-Warning "Failed to import Cert with filename $($cert.file.FullName)" + Write-Warning -Message "Error: $($_.Exception.ErrorRecord)" + Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)" + } + } + else { + try { + $installedCert = Import-PfxCertificate -FilePath $cert.file.FullName -CertStoreLocation $cert.certStore -Exportable -Password $Password + foreach ($ADGroup in $ADGroups.Split("{,}")) { + Write-Host "Setting Permissions" + Set-CertPermissions -certThumprint $installedCert.Thumbprint -user $ADGroup | Out-Null + } + } + catch [System.ComponentModel.Win32Exception] { + Write-Warning "Failed to import Cert with filename $($cert.file.FullName)" + Write-Warning -Message "Error: $($_.Exception.Message)" + Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)" + } + catch { + Write-Warning "Failed to import Cert with filename $($cert.file.FullName)" + Write-Warning -Message "Error: $($_.Exception.ErrorRecord)" + Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)" + } + } + } + } + else { + Write-Warning -Message "File could not be downloaded for certificate with thumbprint $($secretThumbprint)." + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Publish-PodToSecretServer.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Publish-PodToSecretServer.ps1 new file mode 100644 index 0000000..6cf9002 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Publish-PodToSecretServer.ps1 @@ -0,0 +1,148 @@ +function Publish-PodToSecretServer { +<# +.SYNOPSIS + Publish Pod's Certificates to Secret Server. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string[]]$ADGroups, + [Parameter(Mandatory = $false)] + [PSObject]$Pod, + [Parameter (Mandatory = $false)] + [string]$PodName, + [Parameter(Mandatory = $false)] + [string]$SecretServerUserName, + [Parameter(Mandatory = $false)] + [string]$SecretServerPassword, + [Parameter(Mandatory = $false)] + [string]$SecretServerUrl ="https://alkami.secretservercloud.com", + [Parameter(Mandatory = $false)] + [string]$TempFolder = "C:\Temp", + [Parameter(Mandatory = $false)] + [ValidateSet("Production", "Staging")] + [string]$EnvironmentType, + [Parameter(Mandatory = $false)] + [string]$SubFolder + ) + begin { + Import-AWSModule # SSM + + switch ($EnvironmentType) { + "Production" {$RootFolderName = "Production-CertApi"} + "Staging" {$RootFolderName = "Staging-CertApi"} + } + + # Get all parmeters which weren't supplied. + if(!$EnvironmentType) + { + $EnvironmentType = Get-AppSetting Environment.Type + } + + if(!$PodName) + { + $PodName = Get-AppSetting Environment.Name + } + + if(!$SubFolder) + { + $SubFolder = Get-AppSetting Environment.Server + } + + if((!$SecretServerUserName -or !$SecretServerPassword)) + { + if(Test-IsAws) + { + if(!$SecretServerUserName) { + $SecretServerUserName = (Get-SSMParameter -Name "secret_server_api_username" -WithDecryption $true).Value + } + + if(!$SecretServerPassword) { + $SecretServerPassword = (Get-SSMParameter -Name "secret_server_api_password" -WithDecryption $true).Value + } + } + else { + Write-Error "Secret credentials must be manually provided if this is run on a non-AWS machine. They cannot be retrieved from SSM." + } + } + + if (!$Pod) { + $PodName = "$PodName-CertApi" # Tack on something to make the folder name distinct from manually created folders. + $Pod = Export-CertificatesToFileSystem -PodName $PodName -ADGroups $ADGroups + } + + $secretServer = [SecretServerConnection]::new($SecretServerUrl, $SecretServerUserName, $SecretServerPassword) + $UseTwoFactor = $false + $secretServer.Authenticate($UseTwoFactor) + + Add-Type -Assembly System.IO.Compression.FileSystem + } + + process { + + #Zip certficates and their chain + $certificatesFromAllStores = $Pod.Stores.Values.Certificates.Values + Compress-Certificates $certificatesFromAllStores $TempFolder + + #Create folder for this pod + $rootFolderId = $SecretServer.GetFolderIdByName($RootFolderName) + + $podFolderId = $secretServer.GetFolderIdByName($Pod.PodName, $rootFolderId) + if (!$podFolderId) { + $podFolderId = $secretServer.AddFolder($rootFolderId, $Pod.PodName) + } + + $subFolderId = $secretServer.GetFolderIdByName($Subfolder, $podFolderId) + if (!$subFolderId) { + $subFolderId = $secretServer.AddFolder($podFolderId, $Subfolder) + } + + foreach ($store in $Pod.Stores.Keys) { + $storeFolderId = $secretServer.GetFolderIdByName($store, $subFolderId) + if (!$storeFolderId) { + $storeFolderId = $secretServer.AddFolder($subFolderId, $store) + } + $certificates = $Pod.Stores.$store.Certificates.Values + + foreach ($certificate in $certificates) { + + try { + #Process + #Get a new secret template + $templateId = $secretServer.GetSecretTemplateIdByName("CertificateStore") + $secret = $SecretServer.GetSecretTemplateById($templateId, $storeFolderId) + + # Configure the secret + # Each of these two line blocks creates a *reference* to a specific item in the secret.items list. That reference is then set to the correct + # certificate field value, which updates the $secret.items[x].itemValue value. Clear as mud? + $certName = $secret.items | Where-Object {$_.fieldName -eq "Certificate Name"} + $certName.itemValue = $certificate.Name + + $certPassword = $secret.items | Where-Object {$_.fieldName -eq "Import Password"} + $certPassword.itemValue = $certificate.Password + + $certNotes = $secret.items | Where-Object {$_.fieldName -eq "Notes"} + $certNotes.itemValue = "ADGroups with access to this certificate: $($certificate.ADGroups)" + + $certThumbprint = $secret.items | Where-Object {$_.fieldName -eq "Thumbprint"} + $certThumbprint.itemValue = $certificate.Thumbprint + + $certExpirationDate = $secret.items | Where-Object {$_.fieldName -eq "ExperationDate"} # Yes, Expiration is mispelled, because Thycotic. + $certExpirationDate.itemValue = $certificate.ExpirationDate.ToShortDateString() + + $secretName = $certificate.Name, $certificate.thumbprint, $store.ToString()[0] -join "-" + $secretId = $secretServer.CreateSecret($secret, $storeFolderId, $secretName) + + #Upload certificate zip folder + Write-Output "Publishing certificate $($certificate.Name)" + $zipFilePath = $certificate.Folder, ($certificate.Name.Trim() + ".zip") -join "\" + $secretServer.UploadFile($secretId, "pfx-File", $zipFilePath) + } + catch [InvalidOperationException] { + Write-Warning "There was a problem publishing a certificate." + Write-Warning $_ + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Read-AppTierCertificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Read-AppTierCertificates.ps1 new file mode 100644 index 0000000..bd8b7a3 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Read-AppTierCertificates.ps1 @@ -0,0 +1,26 @@ +function Read-AppTierCertificates { +<# +.SYNOPSIS + Reads App Tier Certificates. +#> + + [CmdletBinding()] + Param( + [string]$baseFolder, + [Hashtable[]]$certificatesTable + ) + + $logLead = (Get-LogLeadName); + + [string[]]$usersWhoNeedRights = $null + $usersWhoNeedRights += "IIS_IUSRS" + + foreach ($service in (Get-AppTierServices) | Where-Object {$_.User -notmatch "SYSTEM|REPLACEME"}) { + $usersWhoNeedRights += $service.User + } + + Write-Verbose ("$logLead : Users who need rights read as {0}" -f ($usersWhoNeedRights -join ",")) + Read-Certificates $baseFolder $certificatesTable $usersWhoNeedRights +} + +Set-Alias -name Load-AppTierCertificates -value Read-AppTierCertificates; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Read-Certificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Read-Certificates.ps1 new file mode 100644 index 0000000..713f8d5 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Read-Certificates.ps1 @@ -0,0 +1,100 @@ +function Read-Certificates { +<# +.SYNOPSIS + Reads Certificates. +#> + + [CmdletBinding()] + Param( + [string]$baseFolder, + [Hashtable[]]$certificatesTable, + [string[]]$usersWhoNeedRights + ) + + $logLead = (Get-LogLeadName); + + $localMachineStoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + $trustedPeopleStore = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople + $personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My + $rootStore = [System.Security.Cryptography.X509Certificates.StoreName]::Root + + $folderInfo = @( + + @{ Name = "Root"; Store = $rootStore; Path = (Join-Path $baseFolder "ROOT"); ImportPrivateKey = $false; }, + @{ Name = "Personal"; Store = $personalStore; Path = (Join-Path $baseFolder "Personal"); ImportPrivateKey = $true; }, + @{ Name = "TrustedPeople"; Store = $trustedPeopleStore; Path = (Join-Path $baseFolder "TrustedPeople"); ImportPrivateKey = $false; } + ) + + # Loop through each folder + foreach ($folder in $folderInfo) { + Write-Output ("$logLead : Loading Certificates from {0}" -f $folder.Path) + $certificates = $certificatesTable | Where-Object {$_.FileName.ToUpperInvariant().StartsWith($folder.Path.ToUpperInvariant())} + + # Loop through each certificate found + foreach ($certificate in $certificates) { + if (!(Test-Path $certificate.FileName)) { + Write-Output ("$logLead : Could not locate certificate on disk at {0}" -f $certificate.FileName) + continue; + } + + if ($certificate.FileName.EndsWith(".pfx")) { + # Create an X509Certificate2 Object + $x509Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2( + $certificate.FileName, + $certificate.Password + ) + + if ($folder.ImportPrivateKey) { + Write-Output ("$logLead : Loading certificate {0} (with private key) to store {1}" -f $certificate.FileName, $folder.Store.ToString()) + + # Load the Certificate and Private Key from File + [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore( + $certificate.FileName, + $folder.Store, + $localMachineStoreLocation, + $certificate.Password + ) + + # Grant rights to the private key + $usersWhoNeedRights | ForEach-Object { + + $serviceAccount = $_ + + Write-Output ("$logLead : Granting rights to private key for user {0}" -f $serviceAccount) + + [Alkami.Ops.Common.Cryptography.CertificateHelper]::GrantRightsToPrivateKeys( + $x509Cert.Thumbprint, + $folder.Store, + $localMachineStoreLocation, + $serviceAccount) + } + } + else { + Write-Output ("$logLead : Loading certificate {0} (without private key) to store {1}" -f $certificate.FileName, $folder.Store.ToString()) + + # Load only the Certificate from File + [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateFromPFXToStore( + $certificate.FileName, + $folder.Store, + $localMachineStoreLocation + ) + } + } + elseif ($certificate.FileName.EndsWith(".cer")) { + Write-Output ("$logLead : Loading certificate {0} to store {1}" -f $certificate.FileName, $folder.Store.ToString()) + + # Load the Certificate from File + [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore( + $certificate.FileName, + $folder.Store, + $localMachineStoreLocation + ) + } + else { + Write-Output ("$logLead : Filetype for {0} does not have any logic associated with it. No action taken" -f $certificate.FileName) + } + } + } +} + +Set-Alias -name Load-Certificates -value Read-Certificates; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Read-WebTierCertificates.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Read-WebTierCertificates.ps1 new file mode 100644 index 0000000..9e61d99 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Read-WebTierCertificates.ps1 @@ -0,0 +1,23 @@ +function Read-WebTierCertificates { +<# +.SYNOPSIS + Reads Web Tier Certificates. +#> + + [CmdletBinding()] + Param( + [string]$baseFolder, + [Hashtable[]]$certificatesTable, + [string[]]$usersWhoNeedRights + ) + + $logLead = (Get-LogLeadName); + + [string[]]$usersWhoNeedRights = $null + $usersWhoNeedRights += "IIS_IUSRS" + + Write-Verbose ("$logLead : Users who need rights read as {0}" -f ($usersWhoNeedRights -join ",")) + Read-Certificates $baseFolder $certificatesTable $usersWhoNeedRights +} + +Set-Alias -name Load-WebTierCertificates -value Read-WebTierCertificates; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Remove-Certificate.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Remove-Certificate.ps1 new file mode 100644 index 0000000..83cbd5d --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Remove-Certificate.ps1 @@ -0,0 +1,157 @@ +function Remove-Certificate { +<# + +.SYNOPSIS + Deletes a specified Certificate + +.DESCRIPTION + This function helps locate a specific Certificate to delete based on where it is located and by a distinguishing characteristic. + Otherwise a Certificate object can be inputted into the function to delete as well. + For the function to work as intended, one of the three must be provided... + + FriendlyName [string] + Thumbprint [string] + Certificate [object] + +.PARAMETER + [string] Specifying the CurrentUser or LocalComputer when navigating Certificate locations + +.PARAMETER + [string] Specifying the folder in which to search for a specific Certificate + +.PARAMETER + [string] A descriptory text to distinguish different Certificates. Not always provided by the Certificate + +.PARAMETER + [string] A specific code that singles out a Certificate + +.PARAMETER + [object] This is an item (certificate) that has already been selected to run this function against + +.EXAMPLE + Remove-Certificate -Store TrustedPublisher -FriendlyName 'Alkami Issued Token' + +.EXAMPLE + Get-Item 'Cert:\LocalMachine\TrustedPublisher\E7EAC1F158CB26C5D2B061B9085D65F7CCC4ADAC' | Remove-Certificate + +#> + + [CmdletBinding(DefaultParameterSetName = 'Certificate', SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory, ParameterSetName = "Certificate", ValueFromPipeline)] + [object]$Certificate, + + [Parameter(ParameterSetName = "Thumbprint")] + [string]$Thumbprint, + + [Parameter(ParameterSetName = "FriendlyName")] + [string]$FriendlyName, + + [Parameter()] + [ValidateSet('CurrentUser','LocalMachine')] + [string]$StoreLocation = 'LocalMachine', + + [Parameter()] + [ArgumentCompleter({ + $possibleValues = @('My','TrustedPeople','CA','UserDS','Root','TrustedPublisher', 'AddressBook','Remote Desktop') + return $possibleValues | ForEach-Object { $_ } # Runs through the foreach loop while the user is tabbing through the different fields + })][string]$Store, + + [Parameter()] + [switch]$Force + ) + + $logLead = Get-LogLeadName + + # If the certificate is populated, and is not a string or is explicitly a certificate that data is used for input + if ($null -ne $Certificate ) { + $certType = $Certificate.GetType().Name + + if ($certType -eq "String") { + Write-Error "$logLead : Certificate parameter was passed as an unrecognizable or unacceptable type: [$certType]. Exiting." + throw New-Object System.ArgumentException "Certificate parameter is expected to be a certificate object." + } + if ($certType -ne "X509Certificate2") { + Write-Warning "$logLead : Certificate is of type [$certType]. This is not explicitly an X509Certificate2, but it's not a string either. Proceeding." + } + + $CertificatePSPath = $Certificate.PSPath + if (![string]::IsNullOrWhiteSpace($CertificatePSPath)) { + # Splitting the path to get the $Store and $StoreLocation + $pathSplits = $Certificate.PSParentPath -split '::' + if ($pathSplits[0] -eq "Microsoft.PowerShell.Security\Certificate") { + $CertLocationAndStore = $pathSplits[1] -split "\\" + # Overwriting user input with Certificate input for Store and StoreLocation + $CertStoreLocation = $CertLocationAndStore[0] + $CertStore = $CertLocationAndStore[1] + if (![string]::IsNullOrWhiteSpace($StoreLocation)) { + if ($CertStoreLocation -ne $StoreLocation) { + Write-Warning "$logLead : The Certificate's Store Location [$CertStoreLocation] does not match the inputted Store Location [$StoreLocation]" + Write-Warning "$logLead : Using the Certificate provided Store Location [$CertStoreLocation]" + $StoreLocation = $CertStoreLocation + } + if (![string]::IsNullOrWhiteSpace($Store)) { + if ($CertStore -ne $Store) { + Write-Warning "$logLead : The Certificate's Store [$CertStore] does not match the inputted Store [$Store]" + Write-Warning "$logLead : Using the Certificate provided Store [$CertStore]" + $Store = $CertStore + } + } + } + } + } + $Thumbprint = $Certificate.Thumbprint + } + + $certBasePath = (Join-Path (Join-Path "cert:\" $StoreLocation) $Store) + # Testing to see if the $Store provided is an actual directory + if (!(Test-Path $certBasePath)) { + Write-Warning "$logLead : The specified store [$Store] does not exist on the store location [$StoreLocation]" + Write-Warning "$logLead : No work to do, stopping" + return + } + + $certificates = @() + + if (![string]::IsNullOrWhiteSpace($Thumbprint)) { + # Finding cert based on given Thumbprint + $certificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.Thumbprint -eq $Thumbprint}) + if (Test-IsCollectionNullOrEmpty $certificates) { + Write-Warning "$logLead : No certificate could be found with the thumbprint [$Thumbprint] in the specified store and location [$StoreLocation\$Store]" + Write-Warning "$logLead : No work to do, stopping" + return + } + } elseif(Test-StringIsNullOrWhiteSpace -value $FriendlyName) { + $foundCertificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.FriendlyName -eq $FriendlyName }) + Write-Warning "$logLead : Received no thumbprint, and an empty string for FriendlyName. This has resulted in inadvertent mass certificate removal in the past." + Write-Warning "$logLead : The following is the list of certificates which would be removed, along with their thumbprints." + Write-Warning "$logLead : If you truly intended to remove these certificates, call this function with each thumbprint explicitly:" + $foundCertificates + return + } else { + # Finding cert based on given FriendlyName + $certificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.FriendlyName -eq $FriendlyName }) + if (Test-IsCollectionNullOrEmpty $certificates) { + Write-Warning "$logLead : No certificate could be found with the friendly name [$FriendlyName] in the specified store and location [$StoreLocation\$Store]" + Write-Warning "$logLead : No work to do, stopping" + return + } + } + + if ($Force -or $PSCmdlet.ShouldProcess("Do you want to delete [$($certificates.Count)] certificate(s) with thumbprint(s) [$($certificates.Thumbprint)] in store(s) [$($certificates.PSPath)]")) { + foreach ($cert in $certificates) { + # Checking to see if the Certificate has a Private Key + if ($cert.HasPrivateKey -eq $true) { + Write-Host "$logLead : Private Key Found" + # Removal of Certification and Private Key + Write-Host "$logLead : Removed $($Cert.PSPath) [$($cert.FriendlyName)] and corresponding Private Key" + Remove-Item -Path $Cert.PSPath -DeleteKey -Force + } else { + # If there is no private key, continue to delete the Certificate + Write-Host "$logLead : Private Key Not Found" + Write-Host "$logLead : Removing $($Cert.PSPath) $($cert.FriendlyName)" + Remove-Item -Path $Cert.PSPath -Force + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/Public/Save-CertificatesToDisk.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Save-CertificatesToDisk.ps1 new file mode 100644 index 0000000..a2b814c --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Save-CertificatesToDisk.ps1 @@ -0,0 +1,91 @@ +function Save-CertificatesToDisk { +<# +.SYNOPSIS + Saves Certificates to Disk. +#> + + [CmdletBinding()] + Param( + [Alkami.Ops.SecretServer.Model.Certificate]$cert, + [ref]$savedCertificates, + [string]$downloadFolder + ) + + $logLead = (Get-LogLeadName); + + $rootCertFolder = Join-Path $downloadFolder "ROOT" + $personalCertFolder = Join-Path $downloadFolder "Personal" + $trustedPeopleFolder = Join-Path $downloadFolder "TrustedPeople" + + if (!([System.IO.Directory]::Exists($rootCertFolder))) { + Write-Verbose ("$logLead : Creating root cert folder {0}" -f $rootCertFolder) + New-Item $rootCertFolder -ItemType Directory -Force | Out-Null + } + + if (!([System.IO.Directory]::Exists($personalCertFolder))) { + Write-Verbose ("$logLead : Creating personal cert folder {0}" -f $personalCertFolder) + New-Item $personalCertFolder -ItemType Directory -Force | Out-Null + } + + if (!([System.IO.Directory]::Exists($trustedPeopleFolder))) { + Write-Verbose ("$logLead : Creating trusted people folder {0}" -f $trustedPeopleFolder) + New-Item $trustedPeopleFolder -ItemType Directory -Force | Out-Null + } + + if ($cert.Name -like "*entrust*" -or $cert.Name -like "*identityguard*") { + # Entrust must go in to Trusted People and Root + Write-Verbose ("$logLead : Downloading Entrust certificate to {0}" -f $rootCertFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; } + Write-Verbose ("$logLead : Downloading Entrust certificate to {0}" -f $trustedPeopleFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = ""; } + } + elseif ($cert.Name -like "*root*") { + # If the certificate name contains "root" we will assume it's a root certificate + Write-Verbose ("$logLead : Downloading Root certificate to {0}" -f $rootCertFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; } + } + elseif ($cert.FileName -match "Alkami.+(Issued|Mutual|RPSTS)") { + # Certs for Web <-> App Communication go in TrustedPeople and Personal + Write-Verbose ("$logLead : Downloading Alkami certificate {0} to {1}" -f $cert.FileName, $trustedPeopleFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = $cert.Password; } + Write-Verbose ("$logLead : Downloading Alkami certificate {0} to {1}" -f $cert.FileName, $personalCertFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($personalCertFolder)); Password = $cert.Password; } + } + elseif ($cert.FileName.EndsWith(".zip")) { + # Client Certs are saved in Secret as ZIP files + # We need to unzip to Personal + Write-Verbose ("$logLead : Downloading certificate ZIP file {0} to {1}" -f $cert.FileName, $downloadFolder) + $downloadedZIP = $cert.SaveFileToDisk($downloadFolder) + + $randomFolderName = [System.IO.Path]::GetRandomFileName().Split('.') | Select-Object -First 1 + $unzipFolder = Join-Path $personalCertFolder $randomFolderName + + if (!([System.IO.Directory]::Exists($unzipFolder))) { + Write-Verbose ("$logLead : Creating temporary unzip folder {0}" -f $unzipFolder) + New-Item $unzipFolder -ItemType Directory -Force | Out-Null + } + + Write-Verbose ("$logLead : Unzipping ZIP file contents to {0}" -f $unzipFolder) + [System.IO.Compression.ZipFile]::ExtractToDirectory($downloadedZIP, $unzipFolder) + $savedCertificates.Value += @{FileName = (Get-ChildItem $unzipFolder -Recurse -Include *.PFX | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1 -ExpandProperty FullName); Password = $cert.Password; } + } + elseif ($cert.FileName -like "*trusted*") { + # If the filename contains "trusted" we will assume it's a trusted people certificate + Write-Verbose ("$logLead : Downloading certificate {0} to {1}" -f $cert.FileName, $trustedPeopleFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = ""; } + } + elseif ($cert.FileName.EndsWith(".cer")) { + # Any other .CER files will be saved to ROOT + Write-Verbose ("$logLead : Downloading certificate {0} to {1}" -f $cert.FileName, $rootCertFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; } + } + elseif ($cert.FileName.EndsWith(".pfx")) { + # All .PFX files will be saved to Personal + Write-Verbose ("$logLead : Downloading certificate with private key {0} to {1}" -f $cert.FileName, $personalCertFolder) + $savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($personalCertFolder)); Password = $cert.Password; } + } + else { + Write-Output ("$logLead : Unable to determine what to do with certificate {0} with SecretID {1}" -f $cert.FileName, $cert.Id) + } +} + diff --git a/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.ps1 new file mode 100644 index 0000000..3091c21 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.ps1 @@ -0,0 +1,80 @@ +function Update-CertBindings { +<# +.SYNOPSIS + Updates all sites in IIS using a certificate to a new certificate if the existing certificate's thumbprint matches the value passed in. + + .PARAMETER existingCertThumbprint + + The existing Cert Thumprint. This must be passed in with the spaces "10 11 14 be" + + .PARAMETER replacementCertThumbprint + + The replacement Cert Thumprint. This must be passed in with the spaces "10 11 14 be" +#> + [CmdletBinding()] + Param( + [parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$existingCertThumbprint, + + [parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$replacementCertThumbprint + ) + $existingCertByteArray = $existingCertThumbprint.Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)} + $existingCertThumbprint = $existingCertThumbprint -replace " " + $existingCert = Get-ChildItem -PATH "CERT:\\LocalMachine\My\$existingCertThumbprint" -Recurse + + if (!$existingCert) + { + throw "Unable to find existing cert in the store with the thumbprint $existingCertThumbprint" + } + + $replacementCertByteArray = $replacementCertThumbprint.Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)} + $replacementCertThumbprint = $replacementCertThumbprint -replace " " + $replacementCert = Get-ChildItem -PATH "CERT:\\LocalMachine\My\$replacementCertThumbprint" -Recurse + + if (!$replacementCert) + { + throw "Unable to find replacement cert in the store with the thumbprint $replacementCertThumbprint" + } + + $serverManager = New-Object Microsoft.Web.Administration.ServerManager + + foreach ($site in $serverManager.sites) { + + $applicableBindings = $site.Bindings | Where-Object {$null -ne $_.CertificateHash} + + if ($applicableBindings.Count -eq 0) + { + Write-Host ("Site {0} does not have any existing certificate bindings." -f $site.Name) + } + else + { + foreach ($binding in $applicableBindings) + { + $hash = $binding.CertificateHash + + #Write-Host ("Certificate Hash for site {0} is $hash" -f $binding.CertificateHash) + + if (@(Compare-Object $hash $existingCertByteArray -sync 0).Length -eq 0) + { + Write-Host ("Updating binding for site {0}" -f $site.Name) + $existingBinding = $binding + $existingBinding.CertificateHash = $replacementCertByteArray + + Save-IISServerManagerChanges $serverManager + } + elseif (@(Compare-Object $hash $replacementCertByteArray -sync 0).Length -eq 0) + { + Write-Host ("The binding for site {0} is already using the new certificate" -f $site.Name) + } + else + { + Write-Host ("The binding cert hash did not match the old or new certificate for site {0}." -f $site.Name) + } + } + } + } +} + diff --git a/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.tests.ps1 b/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.tests.ps1 new file mode 100644 index 0000000..5e7f610 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/Public/Update-CertBindings.tests.ps1 @@ -0,0 +1,133 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$exportPassword = "Test" +$exportPath = "c:\temp\CertificateTest" + +Remove-Item $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null +New-Item -ItemType Directory $exportPath -Force | Out-Null + +Describe "Update-CertBindings" { + + BeforeEach { + + Mock -ModuleName $moduleForMock Get-ChildItem { return @{ PsParentPath="Microsoft.PowerShell.Security\Certificate::LocalMachine\My" }} -ParameterFilter { $Path -and $Path -eq "CERT:\\LocalMachine\My\0102030405" } + Mock -ModuleName $moduleForMock Get-ChildItem { return @{ PsParentPath="Microsoft.PowerShell.Security\Certificate::LocalMachine\My" }} -ParameterFilter { $Path -and $Path -eq "CERT:\\LocalMachine\My\1011121314" } + Mock -ModuleName $moduleForMock Get-ChildItem { return $null} -ParameterFilter { $PsParentPath -and !$PsParentPath -eq "CERT:\\0102030405" } + Mock -ModuleName $moduleForMock Save-IISServerManagerChanges {} + } + + Context "When there are bad inputs when calling Update-CertBindings" { + + It "Throws Exception if all skip flags set" { + { Update-CertBindings '' "thumbprint" } | Should Throw + } + + It "Throws Exception if path doesn't exist" { + { Update-CertBindings "thumbprint" '' } | Should Throw + } + } + + Context "When the inputs are valid and the certificates are missing" { + + It "Throws Exception when existing cert not found" { + { Update-CertBindings "99 99 99 99 99" '01 02 03 04 05' } | Should Throw "9999999999" + } + + It "Throws Exception when replacement cert not found" { + { Update-CertBindings "01 02 03 04 05" '99 99 99 99 99' } | Should Throw "9999999999" + } + } + + Context "When the inputs are valid and the certificates exist" { + + It "Updates Certificate Hash with new certificate hash when the site is valid and matches existing cert" { + Mock -ModuleName $moduleForMock New-Object { + @{ + + Sites = @{ + Name = "Test Site" + Bindings = @{ + CertificateHash = "01 02 03 04 05".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)} + } + + } + } + } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"} + + Update-CertBindings "01 02 03 04 05" "10 11 12 13 14" + + Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 1 -Exactly -Scope It + } + + It "Does not Update Certificate Hash when there are no sites" { + + Mock -ModuleName $moduleForMock New-Object { } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"} + + Update-CertBindings "01 02 03 04 05" "10 11 12 13 14" + + Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It + } + + It "Does not Update Certificate Hash when no sites have a certificate binding" { + + Mock -ModuleName $moduleForMock New-Object { + @{ + + Sites = @{ + Name = "Test Site" + Bindings = $null + + } + } + } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"} + + Update-CertBindings "01 02 03 04 05" "10 11 12 13 14" + + Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It + } + + It "Does not update hash when sites hash matches new cert hash" { + Mock -ModuleName $moduleForMock New-Object { + @{ + + Sites = @{ + Name = "Test Site" + Bindings = @{ + CertificateHash = "10 11 12 13 14".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)} + } + + } + } + } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"} + + Update-CertBindings "01 02 03 04 05" "10 11 12 13 14" + + Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It + } + + It "Does not update hash when sites hash does not match existing certificate" { + Mock -ModuleName $moduleForMock New-Object { + @{ + + Sites = @{ + Name = "Test Site" + Bindings = @{ + CertificateHash = "03 04 02 01".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)} + } + + } + } + } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"} + + Update-CertBindings "01 02 03 04 05" "10 11 12 13 14" + + Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Certificates/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Certificates/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Certificates/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.Certificates/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.nuspec b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.nuspec new file mode 100644 index 0000000..c6eaada --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.nuspec @@ -0,0 +1,33 @@ + + + + Alkami.DevOps.Common + $version$ + Alkami Platform Modules - DevOps - Common + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Common + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps Common module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.psd1 b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.psd1 new file mode 100644 index 0000000..dec8aac --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.psd1 @@ -0,0 +1,22 @@ +@{ + RootModule = 'Alkami.DevOps.Common.psm1' + ModuleVersion = '4.2.1' + GUID = '5151a169-c763-489e-8213-b437b7a294b1' + Author = 'SRE,dsage,cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A set of common functions and filters not typically used directly' + FileList = @('Resources\ObjectAsTableTemplate.html') + FormatsToProcess = @('Resources\Formatters\*.ps1xml') + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Configuration','Alkami.Ops.SecretServer','Alkami.DevOps.Certificates' + FunctionsToExport = 'Get-AlkamiAwsProfileList','Get-AlkamiServiceFabricHostnamesByTag','Get-ArmorList','Get-AutomoxAgentPath','Get-AwsCredentialConfiguration','Get-AwsStandardDynamicParameters','Get-CatalogsFromMaster','Get-DesignationTagNameByEnvironment','Get-DynamicAwsProfilesParameter','Get-DynamicAwsRegionParameter','Get-EntrustAdminUrlFromClient','Get-Environment','Get-FileContentHash','Get-HostnamesByEnvironmentName','Get-InstanceHostname','Get-InstancesByTag','Get-IPSTSUrlFromClient','Get-LocalNlbIp','Get-LogDiskUtilization','Get-PackageForInstallation','Get-PodName','Get-SecretsForPod','Get-SecurityGroupPrefix','Get-ServerStatusReport','Get-SlackAction','Get-SlackAttachment','Get-SlackAttachmentFields','Get-SlackMessage','Get-SlackMessageColor','Get-UserCredentialsFromSecretServer','Get-UTF8ContentHash','Invoke-GetAllDesignationsByEnvironmentType','Invoke-GetAllHostsByDesignation','Invoke-GetCurrentStatusByDesignation','Invoke-GetCurrentStatusByHostnames','Invoke-GetDesignationExclusionsByEnvironmentType','Invoke-MaximizeDesignation','Invoke-MinimizeDesignation','Invoke-RemoveShutdownPendingTag','Invoke-StartDesignation','Invoke-StartEnvironment','Invoke-StartServers','Invoke-StopDesignation','Invoke-StopEnvironment','Invoke-StopServers','New-AlkamiServiceFabricClusterByTag','New-WebTierMachineConfigAppSettings','Publish-MessageToSlack','Reset-ASInstanceHealth','Select-AvailableWinRmHosts','Send-ObjectAsHtmlTable','Test-IsTeamCityProcess' + AliasesToExport = 'Create-WebTierMachineConfigAppSettings' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'common') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Common+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Common+Module' +} diff --git a/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.pssproj b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.pssproj new file mode 100644 index 0000000..af0d358 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Alkami.DevOps.Common.pssproj @@ -0,0 +1,114 @@ + + + + Debug + 2.0 + {1bd8fc22-5882-4d5c-8128-81f1d61f8d77} + Exe + MyApplication + MyApplication + Alkami.DevOps.Common + SRE + Alkami Technology + (c) 2017 Alkami Technology. All rights reserved. + A set of common functions and filters not typically used directly + 5151a169-c763-489e-8213-b437b7a294b1 + 0.0.0.1 + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Common") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + Alkami.Ops.Common + {fa9745dd-68ac-4194-9c33-acf19411d357} + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/AlkamiManifest.xml b/Modules/Alkami.DevOps.Common/AlkamiManifest.xml new file mode 100644 index 0000000..c41ff6d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Common + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Common/Private/ConvertTo-AwsCredentialEntry.ps1 b/Modules/Alkami.DevOps.Common/Private/ConvertTo-AwsCredentialEntry.ps1 new file mode 100644 index 0000000..e72aa45 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Private/ConvertTo-AwsCredentialEntry.ps1 @@ -0,0 +1,46 @@ +function ConvertTo-AwsCredentialEntry { + +<# +.SYNOPSIS + Used to convert AWS credentials parsed from file to a standard / friendly format + +.DESCRIPTION + Used to convert AWS credentials parsed from file to a standard / friendly format + +.PARAMETER CredentialData + Credential data read from file by Get-AwsCredentialConfiguration + +.LINK + Get-AwsCredentialConfiguration +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [System.Collections.ArrayList]$CredentialData + ) + begin { + $logLead = Get-LogLeadName + + $defaultTypeName = 'AwsCredentialEntry' + $defaultKeys = @('Name') + $defaultDisplaySet = @('Name', 'role_arn', 'mfa_serial', 'region', 'source_profile') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet', [string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet, $defaultKeyPropertySet) + } + process { + $entryArray = @() + + foreach ($entry in $CredentialData) { + try { + $entry.PSObject.TypeNames.Insert(0, $defaultTypeName) + Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers -InputObject $entry + $entryArray += $entry + } catch { + Write-Warning "$logLead : Could not convert item to AwsCredentialEntry object. Exception: $($_.Exception.Message)" + } + } + + return $entryArray + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.Common/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..db53e64 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Private/VariableDeclarations.ps1 @@ -0,0 +1,32 @@ +# Define Tag Magic Strings +$Global:AlkamiTagKeyEnvironment = "alk:env" +$Global:AlkamiTagKeyService = "alk:service" +$Global:AlkamiTagKeyProject = "alk:project" +$Global:AlkamiTagKeyRole = "alk:role" +$Global:AlkamiTagKeyHostName = "alk:hostname" +$Global:AlkamiTagKeyBootstrap = "alk:bootstrap-phase" +$Global:AlkamiTagKeyInstanceId = "alk:instanceid" + +$Global:AlkamiTagValueEnvironmentDev = "dev" +$Global:AlkamiTagValueEnvironmentQa = "qa" +$Global:AlkamiTagValueEnvironmentSandbox = "sandbox" +$Global:AlkamiTagValueEnvironmentStaging = "staging" +$Global:AlkamiTagValueEnvironmentProd = "prod" +$Global:AlkamiTagValueEnvironmentDR = "dr" +$Global:AlkamiTagValueEnvironmentLTM = "ltm" +$Global:AlkamiTagValueEnvironmentLoadtest = "loadtest" + +$Global:AlkamiDesignationEnvironmentDev = "designation" +$Global:AlkamiDesignationEnvironmentQA = "designation" +$Global:AlkamiDesignationEnvironmentSandbox = "designation" +$Global:AlkamiDesignationEnvironmentStaging = "lane" +$Global:AlkamiDesignationEnvironmentProd = "pod" +$Global:AlkamiDesignationEnvironmentDR = "pod" +$Global:AlkamiDesignationEnvironmentLTM = "designation" +$Global:AlkamiDesignationEnvironmentLoadtest = "designation" + +$Global:AlkamiTagValueRoleApp = "app:app" +$Global:AlkamiTagValueRoleWeb = "web" +$Global:AlkamiTagValueRoleFab = "app:fab" +$Global:AlkamiTagValueRoleMicroservice = "app:microservice" +$Global:AlkamiTagValueRoleEntrust = "app:entrust" \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.ps1 new file mode 100644 index 0000000..6cfeebd --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.ps1 @@ -0,0 +1,120 @@ +function Get-AlkamiAwsProfileList { + +<# +.SYNOPSIS + Retrieves a list of all Alkami AWS profile names. + +.DESCRIPTION + Retrieves a list of all Alkami AWS profile names. Will prepend 'temp-' to the profile names if not running + through a TeamCity process; this logic can be bypassed using the '-RawOutput' argument. + +.PARAMETER RawOutput + [switch] Flag indicating that the profiles should not have 'temp-' prepended to them. + +.PARAMETER IncludeSubsidiaries + [switch] Flag indicating that the profile names for Alkami subsidiaries should also be included. + +.EXAMPLE + Get-AlkamiAwsProfileList + +temp-corp +temp-dev +temp-loadtest +temp-mgmt +temp-mp +temp-prod +temp-qa +temp-sandbox +temp-security +temp-transit +temp-transitnp +temp-workspaces + +.EXAMPLE + Get-AlkamiAwsProfileList -RawOutput + +Corp +Dev +Loadtest +Mgmt +Mp +Prod +Qa +Sandbox +Security +Transit +Transitnp +Workspaces + +.EXAMPLE + Get-AlkamiAwsProfileList -IncludeSubsidiaries + +temp-achdev +temp-corp +temp-dev +temp-loadtest +temp-mgmt +temp-mp +temp-prod +temp-qa +temp-sandbox +temp-security +temp-transit +temp-transitnp +temp-workspaces + +#> + + [OutputType([string[]])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [switch] $RawOutput, + + [Parameter(Mandatory = $false)] + [Alias("All")] + [switch] $IncludeSubsidiaries + ) + + # Define the standard list of AWS profiles for Alkami. + $awsProfileList = @( + "Corp", + "Dev", + "Loadtest", + "Mgmt", + "Mp", + "Prod", + "Qa", + "Sandbox", + "Security", + "Transit", + "Transitnp", + "Workspaces" + ) + + # Define the standard list of AWS profiles for Alkami subsidiaries. + $awsProfileListSubsidiaries = @( + 'AchDev' + 'IgniteDev', + 'IgniteMerchantsProd', + 'IgniteIam', + 'IgniteBanksProd', + 'IgniteBanksTest', + 'IgniteBanksQa' + ) + + $result = $awsProfileList + + if ( $IncludeSubsidiaries ) { + $result += $awsProfileListSubsidiaries + } + + if (( $false -eq ( Test-IsTeamCityProcess ) ) -and ( $false -eq $RawOutput.IsPresent ) ) { + + # Prepend 'temp-' to the profile name if we aren't on TeamCity and the user didn't specify + # that they wanted raw output. + $result = $result.ForEach( { "temp-" + $_.ToLower() } ) + } + + return ( $result | Sort-Object ) +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.tests.ps1 new file mode 100644 index 0000000..de5d9e3 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiAwsProfileList.tests.ps1 @@ -0,0 +1,61 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 + +Describe "Get-AlkamiAwsProfileList" { + + Context "Result Respects TeamCity Process" { + + It "Does Not Prepend 'temp-'" { + + Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $true } + + (Get-AlkamiAwsProfileList) | Should -Contain "Prod" + } + + It "Calls Test-IsTeamCityProcess" { + + Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $true } + + Get-AlkamiAwsProfileList | Out-Null + + Assert-MockCalled -CommandName Test-IsTeamCityProcess -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Common + } + } + + Context "Result Respects 'RawOutput' Flag For Non-TeamCity Processes" { + + It "Prepends 'temp-' By Default" { + + Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false } + + ( Get-AlkamiAwsProfileList ) | Should -Contain 'temp-prod' + } + + It "Does Not Prepend 'temp-' When RawOutput is Specified" { + + Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false } + + ( Get-AlkamiAwsProfileList -RawOutput ) | Should -Contain 'Prod' + } + } + + Context "Result Respects 'IncludeSubsidiaries' Flag" { + + Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false } + + It "Does Not Include AchDev By Default" { + + ( Get-AlkamiAwsProfileList ) | Should -Not -Contain 'temp-achdev' + } + + It "Includes AchDev When IncludeSubsidiaries is Specified" { + + ( Get-AlkamiAwsProfileList -IncludeSubsidiaries ) | Should -Contain 'temp-achdev' + } + + It "Includes AchDev When All is Specified" { + + ( Get-AlkamiAwsProfileList -All ) | Should -Contain 'temp-achdev' + } + } + +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AlkamiServiceFabricHostnamesByTag.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiServiceFabricHostnamesByTag.ps1 new file mode 100644 index 0000000..0abffb1 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AlkamiServiceFabricHostnamesByTag.ps1 @@ -0,0 +1,105 @@ +function Get-AlkamiServiceFabricHostnamesByTag { +<# +.SYNOPSIS + Returns service fabric servers in a pod that are at the minimum bootstrap phase. + Writes an error and returns $null if the requested minimum number of servers cannot be found. + +.PARAMETER designation + The pod tag designation for the cluster. + +.PARAMETER minservers + The minimum number of servers to wait for in the group, before creating the cluster. + +.PARAMETER role + The alk:role tag of the instance to query for, to discover peer servers. + +.PARAMETER minBootstrapPhase + The minimum bootstrap phase of servers that are allowed to join the cluster. 95 (at time of writing) is the ServiceFabric bootstrap phase. + +.PARAMETER timeout + The amount of time (in minutes) this command will poll for valid servers to join into the cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$designation, + + [Parameter(Mandatory = $false)] + [int]$minServers = 5, + + [Parameter(Mandatory = $false)] + [string]$role = "app:fab", + + [Parameter(Mandatory = $false)] + [int]$minBootstrapPhase = 95, + + [Parameter(Mandatory = $false)] + [int]$timeout = 15 + ) + + $loglead = Get-LogLeadName; + + if (($minServers -le 0) -or ($minServers -eq 2)) { + Write-Error "$loglead : Minimum number of servers in SF cluster must be 1, or 3+" + return; + } + + $interval = 15; # Seconds + # $timeout is in minutes. + $numRetries = [Math]::Floor(($timeout * 60) / $interval); + $retries = 0; + + $designationTagName = Get-DesignationTagNameByEnvironment + Write-Host "$logLead : Designation tag for current environment is $designationTagName" + + # Query for ready-to-join peer FAB servers in this pod.; + Write-Host "$loglead : Querying for FAB servers in the pod."; + Write-Host "$logLead : Query Criteria: `n alk:$designationTagName = $designation`n $Global:AlkamiTagKeyRole = $role`n State = Running" + + while ($retries -lt $numRetries) { + + # Look for servers from the pod at the service-fabric-joining state of initialization. + $servers = Get-InstancesByTag -tags @{ "alk:$designationTagName" = $designation; $Global:AlkamiTagKeyRole = $role } + + # Filter by servers that are ready to be joined into the cluster, or are already in the cluster. + # 95 is the userdata bootscrap script phase that joins nodes into the cluster. + Write-Host "$logLead : Looking for instances with a bootstrap phase greater than $minBootstrapPhase" + $servers = $servers | Where-Object { $null -ne ($_.Tag | Where-Object { ($_.Key -eq $Global:AlkamiTagKeyBootstrap) -and ($_.Value -ge $minBootstrapPhase) }) }; + + # Sort them by IP so every server that gets to this step will have the same list in the same order. + $servers = $servers | Sort-Object -Property PrivateIpAddress; + + # Filter out servers that aren't running. + $servers = $servers | Where-Object { $_.State.Name -eq "Running"; } + + # We need at least 3 instances to join into a cluster. + if ($servers.count -ge $minServers) { + # We found the instances we needed. + break; + } + + Write-Verbose "$loglead : Found $($servers.count) ready instances. Need at least $minServers servers to join into a Service Fabric cluster. Retrying in 15s"; + Start-Sleep -s $interval; + $retries++; + } + + if ($retries -eq $numRetries) { + Write-Error "$loglead : Could not discover at least $minServers ready instances to join into a Service Fabric cluster."; + return $null; + } + + Write-Host "$loglead : Found $minServers servers at bootstrap phase $minBootstrapPhase to create a ServiceFabric cluster with."; + + # Build the fqdn's from the private IP's of the servers in the list. + $serverIps = $servers | select-object -ExpandProperty PrivateIpAddress; + $serverNames = @(); + foreach ($ip in $serverIps) { + $hostname = "fab{0}.fh.local" -f $ip.Replace(".", "").Substring(2); + $serverNames += $hostname; + } + + Write-Host "$loglead : Discovered Fabric Server Peers:"; + $serverNames | ForEach-Object { Write-Host $_; }; + + return $serverNames; +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.ps1 new file mode 100644 index 0000000..dacbf69 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.ps1 @@ -0,0 +1,145 @@ +function Get-ArmorList { + +<# +.SYNOPSIS + Retrieve hostnames for an environment in a usable format. + +.DESCRIPTION + A script that allows the user to retrieve all or some of the hosts in an environment, properly formatted for addition to an armor file, or for use in other applications. + +.PARAMETER EnvironmentName + Required Parameter. The moniker associated with the environment (e.g. '12.4', 'Smith') + +.PARAMETER EnvironmentType + Optional Parameter. The type of environment (e.g. 'prod', 'dr'). Defaults to the environment of the server the command is run from. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +APP16115197.fh.local,APP167765.fh.local,APP1697110.fh.local,MIC1676159.fh.local,MIC169629.fh.local,WEB16118134.fh.local,WEB1671254.fh.local,WEB1698191.fh.local + +.PARAMETER Tier + Optional Parameter. Filter to a specific tier: App, Web, Mic, Fab. Defaults to include all tiers if not provided. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'Web' + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +WEB16118134.fh.local,WEB1671254.fh.local,WEB1698191.fh.local + +.PARAMETER Quote + Optional Parameter. Wraps each hostname in doublequotes. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -Quote + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +"APP16115197.fh.local","APP167765.fh.local","APP1697110.fh.local" + +.PARAMETER NoDomain + Optional Parameter. Omits the domain name from the hostnames. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -NoDomain + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +APP16115197,APP167765,APP1697110 + +.PARAMETER List + Optional Parameter. Returns an array of hostnames instead of a comma-delimited string. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -List + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +APP16115197.fh.local +APP167765.fh.local +APP1697110.fh.local + +.PARAMETER IncludeOffline + Optional Parameter. Returns both offline and online hosts. + +.EXAMPLE + Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -IncludeOffline + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +APP16111223.fh.local,APP16115197.fh.local,APP16122230.fh.local,APP167765.fh.local,APP1697110.fh.local + +.PARAMETER ProfileName + Optional Parameter. Specify the AWS profile to use. + +.PARAMETER Region + Optional Parameter. Specify the AWS region to use. + +.EXAMPLE + Get-ArmorList -EnvironmentName 17 -EnvironmentType 'Prod' -Tier 'App' -ProfileName 'temp-prod' -Region 'us-west-2' + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod +APP3210599.fh.local,APP3272118.fh.local,APP327852.fh.local +#> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("Pod")] + [string]$EnvironmentName, + + [Parameter(Mandatory = $false)] + [string]$EnvironmentType, + + [Parameter(Mandatory = $false)] + [ValidateSet("web", "app", "mic", "fab")] + [string]$Tier, + + [Parameter(Mandatory = $false)] + [Switch]$Quote, + + [Parameter(Mandatory = $false)] + [Switch]$NoDomain, + + [Parameter(Mandatory = $false)] + [Switch]$List, + + [Parameter(Mandatory = $false)] + [Switch]$IncludeOffline, + + [Parameter(Mandatory = $false)] + [string]$ProfileName = $null, + + [Parameter(Mandatory = $false)] + [string]$Region = $null + ) + + [string[]] $servers = Get-HostnamesByEnvironmentName -EnvironmentName $EnvironmentName -EnvironmentType $EnvironmentType ` + -ProfileName $ProfileName -Region $Region -IncludeOffline:$IncludeOffline + + if ( ! $NoDomain ) { + + $servers = foreach ( $server in $servers ) { + + "$server.fh.local" + } + } + + if ( $PSBoundParameters.ContainsKey('Tier') ) { + + [string[]] $servers = Get-ServerByType -Server $servers -Type $Tier + } + + if ( $Quote ) { + + $servers = foreach ( $server in $servers ) { + + "`"$server`"" + } + } + + if ( ! $List ) { + + $servers = $servers -join ',' + } + + return $servers +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.tests.ps1 new file mode 100644 index 0000000..7299064 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-ArmorList.tests.ps1 @@ -0,0 +1,137 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ArmorList" { + + Context "Parameter Validation" { + + It "Null Environment Name Should Throw" { + + { Get-ArmorList -EnvironmentName $null } | Should Throw + } + + It "Empty Environment Name Should Throw" { + + { Get-ArmorList -EnvironmentName '' } | Should Throw + } + + It "Invalid Tier Should Throw" { + + { Get-ArmorList -EnvironmentName 'Test' -Tier 'Test' } | Should Throw + } + } + + Context "Result Respects NoDomain Parameter" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + It "Appends Domain By Default" { + + ( Get-ArmorList -EnvironmentName 'Test' ) | Should -BeExactly 'A.fh.local,B.fh.local' + } + + It "Does Not Append Domain When Switch Is Present" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B' + } + } + + Context "Result Respects List Parameter" { + + It "Returns a Comma-Delimited String By Default" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B' + } + + It "Returns a String Type By Default" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) -is [string] | Should -BeTrue + } + + It "Returns An Array When Switch Is Present" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) | Should -BeExactly @( 'A', 'B' ) + } + + It "Returns An Array Type When Switch Is Present" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) -is [array] | Should -BeTrue + } + + It "Returns a String Type By Default When There Is A Single Hostname" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A' ) } -ModuleName $moduleForMock + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) -is [string] | Should -BeTrue + } + + It "Returns An Array Type When Switch Is Present When There Is A Single Hostname" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A' ) } -ModuleName $moduleForMock + + # This test feels worthless, but Powershell is coercing a single element array to a string on return. + # I verified through debug prints that the variable is of type [string[]] all the way up to the return + # statement. Have fun reading https://superuser.com/a/414666 + [string[]] $result = Get-ArmorList -EnvironmentName 'Test' -NoDomain -List + $result -is [array] | Should -BeTrue + } + } + + Context "Result Respects Quote Parameter" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock + + It "Returns Unquoted Values By Default" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B' + } + + It "Returns Quoted Values When Switch Is Present" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -Quote ) | Should -BeExactly '"A","B"' + } + } + + Context "Result Respects Tier Parameter" { + + Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'WEB123', 'APP123', 'MIC123', 'FAB123' ) } -ModuleName $moduleForMock + + It "Returns All Tiers By Default" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) | Should -BeExactly @( 'WEB123', 'APP123', 'MIC123', 'FAB123' ) + } + + It "Returns Only Web Servers When Web Tier Filtering" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Web') | Should -BeExactly @( 'WEB123' ) + } + + It "Returns Only App Servers When App Tier Filtering" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'App') | Should -BeExactly @( 'APP123' ) + } + + It "Returns Only Mic Servers When Mic Tier Filtering" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Mic') | Should -BeExactly @( 'MIC123' ) + } + + It "Returns Only Fab Servers When Fab Tier Filtering" { + + ( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Fab') | Should -BeExactly @( 'FAB123' ) + } + } +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.ps1 new file mode 100644 index 0000000..7454759 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.ps1 @@ -0,0 +1,25 @@ +function Get-AutomoxAgentPath { + +<# +.SYNOPSIS +Returns the full path and filename of the Automox Agent executable + +.DESCRIPTION +Searches Program Files and Program Files x86 for amagent.exe. Returns the full path and filename +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName) + + $automoxAgentDirectory = Get-ChildItem -File -Recurse -Depth 2 -Path @(${env:ProgramFiles(x86)}, $ENV:ProgramFiles) -Filter amagent.exe | Select-Object -First 1 + + if ($null -eq $automoxAgentDirectory) { + + Write-Warning "$logLead : Unable to Locate the Automox Agent Executable" + return $null + } + + return $automoxAgentDirectory.FullName +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.tests.ps1 new file mode 100644 index 0000000..ec9cb44 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AutomoxAgentPath.tests.ps1 @@ -0,0 +1,25 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AutomoxAgentPath" { + + Context "Validation" { + + It "Writes a Warning if No Executable Found" { + + Mock -ModuleName $moduleForMock Get-ChildItem { return $null } + ( Get-AutomoxAgentPath 3>&1 ) -match "Unable to Locate the Automox Agent Executable" | Should -Be $true + } + + It "Returns the FullName Property of the File When Found" { + + Mock -ModuleName $moduleForMock Get-ChildItem { return New-Object PSObject -Property @{ Name="Foo"; FullName="C:\Temp\FooBar\amagent.exe"; } } + Get-AutomoxAgentPath | Should -Be "C:\Temp\FooBar\amagent.exe" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.ps1 new file mode 100644 index 0000000..7319a80 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.ps1 @@ -0,0 +1,144 @@ +function Get-AwsCredentialConfiguration { + +<# +.SYNOPSIS + Used to read/parse the AWS credentials file entries to a queryable object array + +.DESCRIPTION + Used to read/parse the AWS credentials file entries to a queryable object array. Runs parsed values through ConvertTo-AwsCredentialEntry + +.EXAMPLE + Get-AwsCredentialConfiguration + +Name : GrandPooBah +role_arn : arn:aws:iam::123430414321:role/CLI-God-Mode-On +mfa_serial : arn:aws:iam::123410044321:mfa/poobah-cli +region : canada-west-99 +source_profile : default + +Name : Gibberish +role_arn : arn:aws:iam::123495924321:role/CLI-NoGood-DoNothing +mfa_serial : arn:aws:iam::123410044321:mfa/gwhiting-cli +region : mexico-north-1 +source_profile : default + +.LINK + ConvertTo-AwsCredentialEntry +#> + + [CmdletBinding()] + [OutputType([object[]])] + param() + + $logLead = Get-LogLeadName + + $userProfileDirectory = Get-EnvironmentVariable -StoreName Process -Name USERPROFILE + + $configurationSources = @() + $configurationSources += Join-Path -Path $userProfileDirectory -ChildPath ".aws/config" + $configurationSources += Join-Path -Path $userProfileDirectory -ChildPath ".aws/credentials" + # ToDo - Add the AWS SDK Credentials Source + + [System.Collections.ArrayList]$objects = @() + $awsProfile = @{} + $lines = @() + $propertiesToIgnore = @("aws_secret_access_key", "aws_session_token") + + foreach ($source in $configurationSources) { + + if (-NOT (Test-Path -Path $source)) { + + Write-Verbose "$logLead : Credential file does not exist at [$source]" + continue + } + + $fileContent = Get-Content -Path $source + + if (Test-IsCollectionNullOrEmpty -Collection $fileContent) { + + Write-Verbose "$logLead : Credential file at [$source] exists but is empty. Skipping." + continue + } + + $lines += $fileContent + $lines += "EOF" + } + + if (Test-IsCollectionNullOrEmpty -Collection $lines) { + + Write-Warning "$logLead : Unable to locate any configured AWS credentials sources. Have you set up your local profiles?" + return + } + + foreach ($line in $lines) { + + if ($line.StartsWith('#') -or (Test-StringIsNullOrWhitespace -Value $line)) { + + # This is a comment or empty line, ignore it + continue + + } elseif ($line -eq "EOF" -and (-NOT (Test-StringIsNullOrWhitespace -Value $awsProfile.Name))) { + + # Save the final profile or in between file swaps + $objects += $awsProfile + + } elseif ($line.StartsWith('[')) { + + # This is a new profile, push any old profile into the objects array and start fresh + if ($null -ne $awsProfile.Name -and ($null -eq ($objects | Where-Object { $_.Name -eq $awsProfile.Name }))) { + + # $awsProfile object is populated, save it + $objects += $awsProfile + } + + $sanitizedProfileName = $line -replace "\[|\]|(profile\s+)", "" + Write-Verbose "$logLead : Looking for existing parsed profiles named [$sanitizedProfileName]" + $existingProfile = $objects | Where-Object { $_.Name -eq $sanitizedProfileName } + + if ($null -ne $existingProfile) { + + # Load the existing profile to attach properties, where unique + Write-Verbose "$logLead : Duplicate profile name [$sanitizedProfileName] found. First-in properties win in this scenario." + + $awsProfile = $existingProfile + $objects.Remove($existingProfile) + + } else { + + # This is a new profile + $awsProfile = New-Object PSObject -Property @{ Name = $sanitizedProfileName } + } + } else { + + if ($null -eq $awsProfile.Name) { + + # We skip this step unless we're anchored to a profile + # to avoid picking up trash or duplicate properties + continue + } + + # The only things left are valid properties to attach to an object + # Unless we're specifically ignoring them + $splits = $line -split '=' + $propertyName = $splits[0].Trim() + + if ($propertiesToIgnore -contains $propertyName) { + + # This is a sensitive property that will never be recorded + Write-Verbose "$logLead : Skipping excluded property [$propertyName] for profile [$($awsProfile.Name)]" + continue + + } elseif ($null -ne $awsProfile.$propertyName) { + + # This property has been recorded already. It is a duplicate, and first read wins + Write-Verbose ("$logLead : Skipping duplicate property [$propertyName] for profile [$($awsProfile.Name)] - existing value: [$($awsProfile.$propertyName)]") + continue + } + + $propertyValue = $splits[1].Trim() + Add-Member -InputObject $awsProfile -NotePropertyName $propertyName -NotePropertyValue $propertyValue -Force + } + } + + return ($objects | ConvertTo-AwsCredentialEntry | Sort-Object -Property Name) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.tests.ps1 new file mode 100644 index 0000000..7311444 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AwsCredentialConfiguration.tests.ps1 @@ -0,0 +1,114 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AwsCredentialConfiguration" { + + # This is a private function, so Pester has a fit + function ConvertTo-AwsCredentialEntry { param([System.Collections.ArrayList]$CredentialData) } + + Context "Logic" { + + It "Reads from Both Text-Based Credentials Files" { + + $expectedFileReads = @("credentials", "config") + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith {} + + Get-AwsCredentialConfiguration | Should -BeNullOrEmpty + + foreach ($fileRead in $expectedFileReads) { + + Assert-MockCalled -CommandName Get-Content -Scope It -Times 1 -Exactly -ParameterFilter { $Path -match "$fileRead$"} + } + } + + It "Writes a Warning and Exits Early if No Credentials Files Found" { + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $false } + + Get-AwsCredentialConfiguration -Verbose + Assert-MockCalled -CommandName Write-Warning -Scope It -Times 1 -Exactly -ParameterFilter { $Message -match "Unable to locate any configured AWS credentials sources" } + } + + It "Writes a Warning and Exits Early if Credentials Files Found but Empty" { + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + + Get-AwsCredentialConfiguration -Verbose + Assert-MockCalled -CommandName Write-Warning -Scope It -Times 1 -Exactly -ParameterFilter { $Message -match "Unable to locate any configured AWS credentials sources" } + } + + It "Parses the File Contents Appropriately" { + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[ErMerGerd]","role_arn=some_value","source_profile=another_value","region=some_region","mfa_serial=12345") } ` + -ParameterFilter { $Path -Match "credentials" } + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[AllInAll]","role_arn=Its","source_profile=Just","region=Another","mfa_serial=BrickInTheWall") } ` + -ParameterFilter { $Path -Match "config" } + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ } + + $result = Get-AwsCredentialConfiguration + $result | Should -HaveCount 2 + $firstResult = $result | Select -First 1 + $lastResult = $result | Select -Last 1 + + $firstResult.Name | Should -BeExactly "AllInAll" + $firstResult.role_arn | Should -BeExactly "Its" + $firstResult.source_profile | Should -BeExactly "Just" + $firstResult.region | Should -BeExactly "Another" + $firstResult.mfa_serial | Should -BeExactly "BrickInTheWall" + + $lastResult.Name | Should -BeExactly "ErMerGerd" + $lastResult.role_arn | Should -BeExactly "some_value" + $lastResult.source_profile | Should -BeExactly "another_value" + $lastResult.region | Should -BeExactly "some_region" + $lastResult.mfa_serial | Should -BeExactly "12345" + } + + It "Merges Profile Properties Preferring First In When Overlap Occurs" { + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[Shmoo]","role_arn=first_value","source_profile=first_value") } ` + -ParameterFilter { $Path -Match "config" } + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[Shmoo]","role_arn=second_value","region=a_value_not_in_object_one" ) } ` + -ParameterFilter { $Path -Match "credentials" } + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ } + + $result = Get-AwsCredentialConfiguration + $result | Should -HaveCount 1 + $result.role_arn | Should -Be "first_value" + $result.source_profile | Should -BeExactly "first_value" + $result.region | Should -BeExactly "a_value_not_in_object_one" + $result.mfa_serial | Should -BeNullOrEmpty + } + + It "Refuses to Return Properties Defined as Sensitive" { + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ } + + $propertiesToIgnore = @("aws_secret_access_key", "aws_session_token") + foreach ($property in $propertiesToIgnore) { + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[OhSay]","role_arn=Can","source_profile=You","$property=See") } ` + -ParameterFilter { $Path -Match "config" } + + $result = Get-AwsCredentialConfiguration + $result[0].$property | Should -BeNullOrEmpty + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.ps1 new file mode 100644 index 0000000..8f81863 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.ps1 @@ -0,0 +1,91 @@ +function Get-AwsStandardDynamicParameters { + +<# +.SYNOPSIS + Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles and Alkami-supported Regions + +.DESCRIPTION + Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles and Alkami-supported Regions as DynamicParameters in functions. + +.PARAMETER RegionParameterName + The parameter name to set on the Region parameter. Defaults to Region + +.PARAMETER RegionParameterSetName + The parameter set to associate the dynamic Region parameter with. Defaults to all parameter sets. + +.PARAMETER RegionParameterRequired + Whether or not the Region parameter is mandatory. Defaults to false. + +.PARAMETER ProfileParameterName + The parameter name to set on the Profile Name parameter. Defaults to ProfileName + +.PARAMETER ProfileParameterSetName + The parameter set to associate the dynamic ProfileName parameter with. Defaults to all parameter sets. + +.PARAMETER ProfileParameterRequired + Whether or not the ProfileName parameter is mandatory. Defaults to false. + +.EXAMPLE + Get-AwsStandardDynamicParameters + +Key Value +--- ----- +Region System.Management.Automation.RuntimeDefinedParameter +Profile System.Management.Automation.RuntimeDefinedParameter + +.LINK + Get-DynamicAwsProfilesParameter + +.LINK + Get-DynamicAwsRegionParameter +#> + + [Cmdletbinding()] + [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] + param( + + [Parameter(Mandatory=$false)] + [string]$RegionParameterName = "Region", + + [Parameter(Mandatory=$false)] + [string]$RegionParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory=$false)] + [switch]$RegionParameterRequired, + + [Parameter(Mandatory=$false)] + [string]$ProfileParameterName = "ProfileName", + + [Parameter(Mandatory=$false)] + [string]$ProfileParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory=$false)] + [switch]$ProfileParameterRequired + ) + + $regionDynamicParams = @{ + + "DynamicParameterName" = $RegionParameterName; + "ParameterSetName" = $RegionParameterSetName; + "IsMandatoryParameter" = $RegionParameterRequired + } + + $regionRuntimeParameter = Get-DynamicAwsRegionParameter @regionDynamicParams + + $profileDynamicParams = @{ + + "DynamicParameterName" = $ProfileParameterName; + "ParameterSetName" = $ProfileParameterSetName; + "IsMandatoryParameter" = $ProfileParameterRequired + } + + $profileRuntimeParameter = Get-DynamicAwsProfilesParameter @profileDynamicParams + + # The Dynamic params functions return dictionaries so they can be used independently if needed + # We will add both returned values to a new Dictionary and return it for use by the calling function + $runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $runtimeParameterDictionary.Add($RegionParameterName, $($regionRuntimeParameter.Values[0])) + $runtimeParameterDictionary.Add($ProfileParameterName, $($profileRuntimeParameter.Values[0])) + + return $runtimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.tests.ps1 new file mode 100644 index 0000000..fdc5df0 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-AwsStandardDynamicParameters.tests.ps1 @@ -0,0 +1,79 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AwsStandardDynamicParameters" { + + Context "Logic" { + + Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return @( + (New-Object PSObject -Property @{ "Name"="FogoDeChao"; }), + (New-Object PSObject -Property @{ "Name"="12Cuts"; }) + )} + + Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @( + "mexico-south-55", + "australia-west-99" + )} + + It "Profile Uses the Specified Parameter Name" { + + $testParamName = "OnlyYouCanMakeAllThisWorldSeemRight" + + $params = Get-AwsStandardDynamicParameters -ProfileParameterName $testParamName + $profileParam = $params.Item($testParamName) + $profileParam | Should -Not -BeNullOrEmpty + $profileParam.Attributes.HelpMessage -like "*Profile*" | Should -BeTrue + } + + It "Region Uses the Specified Parameter Name" { + + $testParamName = "OnlyYouCanMakeTheDarknessBright" + + $params = Get-AwsStandardDynamicParameters -RegionParameterName $testParamName + $regionParam = $params.Item($testParamName) + $regionParam | Should -Not -BeNullOrEmpty + $regionParam.Attributes.HelpMessage -like "*Region*" | Should -BeTrue + } + + It "Profile Uses the Specified ParameterSet Name" { + + $testParamSetName = "OnlyYouAndYouAloneCanThrillMeLikeYouDo" + + $params = Get-AwsStandardDynamicParameters -ProfileParameterSetName $testParamSetName + $profileParam = $params.Item("ProfileName") + $profileParam | Should -Not -BeNullOrEmpty + $profileParam.Attributes.ParameterSetName | Should -BeExactly $testParamSetName + } + + It "Region Uses the Specified ParameterSet Name" { + + $testParamSetName = "AndFillMyHeartWithLoveForOnlyYou" + + $params = Get-AwsStandardDynamicParameters -RegionParameterSetName $testParamSetName + $regionParam = $params.Item("Region") + $regionParam | Should -Not -BeNullOrEmpty + $regionParam.Attributes.ParameterSetName | Should -BeExactly $testParamSetName + } + + It "Profile Is a Mandatory Parameter if Specified" { + + $params = Get-AwsStandardDynamicParameters -ProfileParameterRequired + $profileParam = $params.Item("ProfileName") + $profileParam | Should -Not -BeNullOrEmpty + $profileParam.Attributes.Mandatory | Should -BeTrue + } + + It "Region Is a Mandatory Parameter if Specified" { + + $params = Get-AwsStandardDynamicParameters -RegionParameterRequired + $regionParam = $params.Item("Region") + $regionParam | Should -Not -BeNullOrEmpty + $regionParam.Attributes.Mandatory | Should -BeTrue + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-CatalogsFromMaster.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-CatalogsFromMaster.ps1 new file mode 100644 index 0000000..2c9d448 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-CatalogsFromMaster.ps1 @@ -0,0 +1,58 @@ +function Get-CatalogsFromMaster { +<# +.SYNOPSIS + Returns a collection of tenants from the master database as a hashtable +#> + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = $false)] + [string]$ConnectionString + ) + + if ([String]::IsNullOrEmpty($ConnectionString)) { + $masterConnectionString = Get-MasterConnectionString + } + else { + $masterConnectionString = $ConnectionString + } + + $conn = New-Object System.Data.SqlClient.SqlConnection + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + + $conn.ConnectionString = $conStrBuilder.ToString() + + [hashtable[]]$databases = @() + + try { + $conn.Open() + $query = New-Object System.Data.SqlClient.SqlCommand("SELECT Name, BankUrlSignatures, BankAdminUrlSignatures, DataSource, Catalog FROM dbo.Tenant", $conn) + $results = $query.ExecuteReader() + + if (!$results.HasRows) { + Write-Warning (" No rows were returned from the tenant table.`nServer: {0}`nDatabase: {1}" -f $conStrBuilder.DataSource, $conStrBuilder.InitialCatalog) + return $null + } + + while ($results.Read()) { + $tenantConStringBuilder = $conStrBuilder + $tenantConStringBuilder.'Data Source' = $results.Item(3) + $tenantConStringBuilder.'Initial Catalog' = $results.Item(4) + $databases += @{Name = $results.Item(0); Signature = $results.Item(1); AdminSignature = $results.Item(2); DataSource = $results.Item(3); Catalog = $results.Item(4); ConnectionString = $tenantConStringBuilder.ToString()} + } + + return $databases + } + catch { + Write-Warning "An exception occurred while trying to pull tenants from the master database" + Write-Warning $_ | Format-List -Force + return $null + } + finally { + if ($conn.State -ne [System.Data.ConnectionState]::Closed) { + $conn.Close() + } + + $conn = $null + } +} + diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.ps1 new file mode 100644 index 0000000..d541008 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.ps1 @@ -0,0 +1,91 @@ +function Get-DesignationTagNameByEnvironment { + + <# +.SYNOPSIS +Looks up the appropriate designation string for a given environment + +.DESCRIPTION +Looks up the appropriate designation string such as Pod, Lane, Box, or Designation for a given environment +Will use the current server's environment for lookup if the machine is an EC2 and no environment parameter has +been provided + +.PARAMETER EnvironmentType +[String] An optional parameter to look up the string for a particular environment type. When supplied will override +the current instance value if the instance is in AWS. Is required for execution on non-EC2 machines + +.EXAMPLE + +Get-DesignationTagNameByEnvironment + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prod +pod + +.EXAMPLE + +Get-DesignationTagNameByEnvironment "prodshared" + +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prodshared +pod + +#> + + [CmdletBinding()] + param( + [ValidateSet("dev", "qa", "staging", "prod", "dr", "sandbox", "devshared", "qashared", "stagingshared", "prodshared", "drshared", "sandboxshared","ltm","ltmshared")] + [Parameter(Mandatory = $false)] + [string]$environmentType + ) + + $logLead = (Get-LogLeadName) + $checkCurrentInstanceTags = Test-IsAws + $parameterProvided = !([String]::IsNullOrEmpty($environmentType)) + + if (!$checkCurrentInstanceTags -and !$parameterProvided) { + + Write-Warning "$logLead : Current server is not in AWS and no environment type name is provided. Execution cannot continue" + return $null + } + + if ($checkCurrentInstanceTags -and !$parameterProvided) { + + Write-Verbose "$logLead : Current host is in AWS and no Environment Type provided. Getting the current instance's $Global:AlkamiTagKeyEnvironment tag" + $environmentTagValue = Get-CurrentInstanceTags $Global:AlkamiTagKeyEnvironment -ValueOnly -ErrorAction Continue + + if ([String]::IsNullOrEmpty($environmentTagValue)) { + + Write-Warning "$logLead : Current server is not configured with the $Global:AlkamiTagKeyEnvironment tag or it could not be retrieved. Execution cannot continue" + return $null + } + + Write-Verbose "$logLead : Using tag value $environmentTagValue for lookup" + } + else { + + Write-Verbose "$logLead : Using parameter value $environmentType for lookup" + $environmentTagValue = $environmentType + } + + Write-Host "$logLead : Checking designation value for environment $environmentTagValue" + $lookupValue = switch ($environmentTagValue) { + + "$Global:AlkamiTagValueEnvironmentProd" { $Global:AlkamiDesignationEnvironmentProd } + ("$Global:AlkamiTagValueEnvironmentProd" + "shared") { $Global:AlkamiDesignationEnvironmentProd } + "$Global:AlkamiTagValueEnvironmentDR" { $Global:AlkamiDesignationEnvironmentDR } + ("$Global:AlkamiTagValueEnvironmentDR" + "shared") { $Global:AlkamiDesignationEnvironmentDR } + "$Global:AlkamiTagValueEnvironmentStaging" { $Global:AlkamiDesignationEnvironmentStaging } + ("$Global:AlkamiTagValueEnvironmentStaging" + "shared") { $Global:AlkamiDesignationEnvironmentStaging } + "$Global:AlkamiTagValueEnvironmentSandbox" { $Global:AlkamiDesignationEnvironmentSandbox } + ("$Global:AlkamiTagValueEnvironmentSandbox" + "shared") { $Global:AlkamiDesignationEnvironmentSandbox } + "$Global:AlkamiTagValueEnvironmentDev" { $Global:AlkamiDesignationEnvironmentDev } + ("$Global:AlkamiTagValueEnvironmentDev" + "shared") { $Global:AlkamiDesignationEnvironmentDev } + "$Global:AlkamiTagValueEnvironmentQa" { $Global:AlkamiDesignationEnvironmentQA } + ("$Global:AlkamiTagValueEnvironmentQa" + "shared") { $Global:AlkamiDesignationEnvironmentQA } + "$Global:AlkamiTagValueEnvironmentLTM" { $Global:AlkamiDesignationEnvironmentLTM } + ("$Global:AlkamiTagValueEnvironmentLTM" + "shared") { $Global:AlkamiDesignationEnvironmentLTM } + "$Global:AlkamiTagValueEnvironmentLoadtest" { $Global:AlkamiDesignationEnvironmentLoadtest } + ("$Global:AlkamiTagValueEnvironmentLoadtest" + "shared") { $Global:AlkamiDesignationEnvironmentLoadtest } + default { "Unknown" } + } + + return $lookupValue +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.tests.ps1 new file mode 100644 index 0000000..3ab20ef --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DesignationTagNameByEnvironment.tests.ps1 @@ -0,0 +1,126 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-DesignationTagNameByEnvironment" { + + Context "Error and Parameter Handling" { + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + It "Does Not Execute if No Parameter Provided and Not in AWS" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + + Get-DesignationTagNameByEnvironment | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Current server is not in AWS" } + } + + It "Does Not Lookup Instance Tags if a Parameter is Provided" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { } + + Get-DesignationTagNameByEnvironment "prod" | Out-Null + Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Pulls Current Instance Tags if in AWS And No Parameter is Supplied" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { } + + Get-DesignationTagNameByEnvironment | Out-Null + Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Returns Unknown if the Tags Have an Unexpected Value for Environment" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return @{ + Key=$Global:AlkamiTagKeyEnvironment;Value="FoobarDoopityDo"; + }} + + Get-DesignationTagNameByEnvironment | Should -Be "Unknown" + Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Throws if an Unknown Value is Passed as a Parameter" { + + { Get-DesignationTagNameByEnvironment "IfSomeoneAddsThisToTheValidateSetIllFirethem" } | Should -Throw + } + } + + Context "Happy Path" { + + It "Returns the Expected Values When Parameters are Provided" { + + Get-DesignationTagNameByEnvironment "prod" | Should -Be $Global:AlkamiDesignationEnvironmentProd + Get-DesignationTagNameByEnvironment "prodshared" | Should -Be $Global:AlkamiDesignationEnvironmentProd + + Get-DesignationTagNameByEnvironment "dr" | Should -Be $Global:AlkamiDesignationEnvironmentProd + Get-DesignationTagNameByEnvironment "drshared" | Should -Be $Global:AlkamiDesignationEnvironmentProd + + Get-DesignationTagNameByEnvironment "dev" | Should -Be $Global:AlkamiDesignationEnvironmentDev + Get-DesignationTagNameByEnvironment "devshared" | Should -Be $Global:AlkamiDesignationEnvironmentDev + + Get-DesignationTagNameByEnvironment "qa" | Should -Be $Global:AlkamiDesignationEnvironmentQA + Get-DesignationTagNameByEnvironment "qashared" | Should -Be $Global:AlkamiDesignationEnvironmentQA + + Get-DesignationTagNameByEnvironment "sandbox" | Should -Be $Global:AlkamiDesignationEnvironmentSandbox + Get-DesignationTagNameByEnvironment "sandboxshared" | Should -Be $Global:AlkamiDesignationEnvironmentSandbox + + Get-DesignationTagNameByEnvironment "staging" | Should -Be $Global:AlkamiDesignationEnvironmentStaging + Get-DesignationTagNameByEnvironment "stagingshared" | Should -Be $Global:AlkamiDesignationEnvironmentStaging + } + + It "Returns the Expected Values When Tags are Queried" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue } + + $Global:MockedEnvironmentValue = "prod" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd + + $Global:MockedEnvironmentValue = "prodshared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd + + $Global:MockedEnvironmentValue = "dr" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd + + $Global:MockedEnvironmentValue = "drshared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd + + $Global:MockedEnvironmentValue = "dev" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentDev + + $Global:MockedEnvironmentValue = "devshared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentDev + + $Global:MockedEnvironmentValue = "qa" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentQA + + $Global:MockedEnvironmentValue = "qashared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentQA + + $Global:MockedEnvironmentValue = "sandbox" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentSandbox + + $Global:MockedEnvironmentValue = "sandboxshared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentSandbox + + $Global:MockedEnvironmentValue = "staging" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentStaging + + $Global:MockedEnvironmentValue = "stagingshared" + Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentStaging + + Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.ps1 new file mode 100644 index 0000000..0678175 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.ps1 @@ -0,0 +1,80 @@ +function Get-DynamicAwsProfilesParameter { + +<# +.SYNOPSIS + Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles + +.DESCRIPTION + Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles for use as a DynamicParameter in functions. Does + not retrieve profiles for any AWSSDK configurations. + +.PARAMETER DynamicParameterName + The parameter name to set on the return value. Defaults to ProfileName + +.PARAMETER ParameterSetName + The parameter set to associate the dynamic parameter with. Defaults to all parameter sets. + +.PARAMETER IsMandatoryParameter + Wheter or not the parameter is mandatory. Defaults to false. + +.EXAMPLE + Get-DynamicAwsProfilesParameter + +Key Value +--- ----- +ProfileName System.Management.Automation.RuntimeDefinedParameter + +.EXAMPLE + + Get-DynamicAwsProfilesParameter -DynamicParameterName "HooDoggie" + +Key Value +--- ----- +HooDoggie System.Management.Automation.RuntimeDefinedParameter + +.LINK + Get-AwsCredentialConfiguration +#> + + [CmdletBinding()] + [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] + param( + + [Parameter(Mandatory = $false)] + [string]$DynamicParameterName = "ProfileName", + + [Parameter(Mandatory = $false)] + [string]$ParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory = $false)] + [switch]$IsMandatoryParameter + ) + + # Define the Paramater Attributes + $runtimeParameterDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary + $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + $parameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute + $parameterAttribute.Mandatory = $IsMandatoryParameter + $parameterAttribute.ParameterSetName = $ParameterSetName + $parameterAttribute.HelpMessage = "The Local AWS Credential Profile Name to Use for Requests" + $attributeCollection.Add($parameterAttribute) + + # Generate and add the ValidateSet + # Do not print warnings because this may be loaded/evaluated on servers without any profiles + $profileNames = Get-AwsCredentialConfiguration -WarningAction SilentlyContinue | Select-Object -ExpandProperty Name + + # If no profiles are found, set the only available value to NoLocalProfilesFound + if (Test-IsCollectionNullOrEmpty -Collection $profileNames) { + + $profileNames = @( "NoLocalProfilesFound" ) + } + + $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($profileNames) + $attributeCollection.Add($validateSetAttribute) + + # Create the dynamic parameter + $runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($DynamicParameterName, [string], $attributeCollection) + $runtimeParameterDictionary.Add($DynamicParameterName, $RuntimeParameter) + return $runtimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.tests.ps1 new file mode 100644 index 0000000..e84e6e6 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsProfilesParameter.tests.ps1 @@ -0,0 +1,53 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-DynamicAwsProfilesParameter" { + + Context "Logic" { + + Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return @( + (New-Object PSObject -Property @{ "Name"="FogoDeChao"; }), + (New-Object PSObject -Property @{ "Name"="12Cuts"; }) + )} + + It "Uses the Specified Parameter Name" { + + $testParamName = "SupaFly" + + $param = Get-DynamicAwsProfilesParameter -DynamicParameterName $testParamName + $param.Keys | Should -BeExactly $testParamName + } + + It "Uses the Specified ParameterSet Name" { + + $testParamSetName = "MisterP" + + $param = Get-DynamicAwsProfilesParameter -ParameterSetName $testParamSetName + $param.Values.Attributes.ParameterSetName | Should -BeExactly $testParamSetName + } + + It "Is a Mandatory Parameter if Specified" { + + $param = Get-DynamicAwsProfilesParameter -IsMandatoryParameter + $param.Values.Attributes.Mandatory | Should -BeTrue + } + + It "Creates a Validate Set with Valid AWS Profile Names" { + + $param = Get-DynamicAwsProfilesParameter + $param.Values.Attributes.ValidValues | Should -Be @( "FogoDeChao", "12Cuts") + } + + It "Returns a Single Invalid Parameter if None Can be Read from Local Configuration" { + + Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return $null } + $param = Get-DynamicAwsProfilesParameter + $param.Values.Attributes.ValidValues | Should -Be @( "NoLocalProfilesFound" ) + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.ps1 new file mode 100644 index 0000000..809552b --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.ps1 @@ -0,0 +1,71 @@ +function Get-DynamicAwsRegionParameter { + +<# +.SYNOPSIS + Returns a RuntimeDefinedParameterDictionary with all Alkami-permitted AWS Regions + +.DESCRIPTION + Returns a RuntimeDefinedParameterDictionary with all Alkami-permitted AWS Regions for use as a DynamicParameter in functions. + +.PARAMETER DynamicParameterName + The parameter name to set on the return value. Defaults to Region + +.PARAMETER ParameterSetName + The parameter set to associate the dynamic parameter with. Defaults to all parameter sets. + +.PARAMETER IsMandatoryParameter + Wheter or not the parameter is mandatory. Defaults to false. + +.EXAMPLE + Get-DynamicAwsRegionParameter + +Key Value +--- ----- +Region System.Management.Automation.RuntimeDefinedParameter + +.EXAMPLE + + Get-DynamicAwsRegionParameter -DynamicParameterName "OhNoHeDidnt" + +Key Value +--- ----- +OhNoHeDidnt System.Management.Automation.RuntimeDefinedParameter + +.LINK + Get-SupportedAwsRegions +#> + + [CmdletBinding()] + [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] + param( + + [Parameter(Mandatory = $false)] + [string]$DynamicParameterName = "Region", + + [Parameter(Mandatory = $false)] + [string]$ParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory = $false)] + [switch]$IsMandatoryParameter + ) + + # Define the Paramater Attributes + $runtimeParameterDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary + $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + $parameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute + $parameterAttribute.Mandatory = $IsMandatoryParameter + $parameterAttribute.ParameterSetName = $ParameterSetName + $parameterAttribute.HelpMessage = "The AWS Region to Use for Requests" + $attributeCollection.Add($parameterAttribute) + + # Generate and add the ValidateSet + $supportedRegions = Get-SupportedAwsRegions + $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($supportedRegions) + $attributeCollection.Add($validateSetAttribute) + + # Create the dynamic parameter + $runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($DynamicParameterName, [string], $attributeCollection) + $runtimeParameterDictionary.Add($DynamicParameterName, $RuntimeParameter) + return $runtimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.tests.ps1 new file mode 100644 index 0000000..6bbd5b4 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-DynamicAwsRegionParameter.tests.ps1 @@ -0,0 +1,46 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-DynamicAwsRegionParameter" { + + Context "Logic" { + + Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @( + "mexico-south-55", + "australia-west-99" + )} + + It "Uses the Specified Parameter Name" { + + $testParamName = "SupaFly" + + $param = Get-DynamicAwsRegionParameter -DynamicParameterName $testParamName + $param.Keys | Should -BeExactly $testParamName + } + + It "Uses the Specified ParameterSet Name" { + + $testParamSetName = "wOne" + + $param = Get-DynamicAwsRegionParameter -ParameterSetName $testParamSetName + $param.Values.Attributes.ParameterSetName | Should -BeExactly $testParamSetName + } + + It "Is a Mandatory Parameter if Specified" { + + $param = Get-DynamicAwsRegionParameter -IsMandatoryParameter + $param.Values.Attributes.Mandatory | Should -BeTrue + } + + It "Creates a Validate Set with Valid AWS Profile Names" { + + $param = Get-DynamicAwsRegionParameter + $param.Values.Attributes.ValidValues | Should -Be @( "mexico-south-55", "australia-west-99") + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-EntrustAdminUrlFromClient.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-EntrustAdminUrlFromClient.ps1 new file mode 100644 index 0000000..44a1715 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-EntrustAdminUrlFromClient.ps1 @@ -0,0 +1,17 @@ +function Get-EntrustAdminUrlFromClient { +<# +.SYNOPSIS + Returns the Entrust Admin URL based on a Client Database object +#> + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = $true)] + [PSObject]$Client + ) + $queryString = "SELECT s.Value FROM core.ItemSetting s JOIN core.Item i on i.ID = s.ItemID JOIN core.Provider p on p.ID = i.ParentId WHERE REPLACE(p.AssemblyInfo, ' ', '') = " + + "'Alkami.Security.Provider.User.Entrust.Provider,Alkami.Security.Provider.User.Entrust' AND s.Name = 'EntrustAdminUrl'" + + $url = Invoke-QueryOnClientDatabase $client $queryString + return ($url -ireplace "Service/services/AdminServiceV\d", "/do") +} + diff --git a/Modules/Alkami.DevOps.Common/Public/Get-Environment.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-Environment.ps1 new file mode 100644 index 0000000..d78ff90 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-Environment.ps1 @@ -0,0 +1,84 @@ +function Get-Environment { + <# +.SYNOPSIS + Tries to determine the Environment type (AWS, QA, Prod, Staging, etc.) using environment details +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + $logLead = (Get-LogLeadName); + $unknown = "UNKNOWN" + + # Get Environment From Tags if Possible + if (Test-IsAws) { + + Write-Verbose "$logLead : Hosting provider is AWS -- checking tags to determine environment" + + $serverEnvironment = Get-CurrentInstanceTags ($Global:AlkamiTagKeyEnvironment) -ValueOnly + + if ([String]::IsNullOrEmpty($serverEnvironment)) { + + Write-Warning "$logLead : Could not determine environment from the $Global:AlkamiTagKeyEnvironment tag. Value returned: <$serverEnvironment>" + return $unknown + } + + $validTagValues = @( + $Global:AlkamiTagValueEnvironmentProd, + $Global:AlkamiTagValueEnvironmentStaging, + $Global:AlkamiTagValueEnvironmentQa, + $Global:AlkamiTagValueEnvironmentDr, + $Global:AlkamiTagValueEnvironmentSandbox, + $Global:AlkamiTagValueEnvironmentDev, + $Global:AlkamiTagValueEnvironmentLTM, + $Global:AlkamiTagValueEnvironmentLoadtest + ) + + if ($serverEnvironment -notin $validTagValues) { + + Write-Warning "$logLead : Unknown tag value '$serverEnvironment' identified. What is going on with your tags?" + return $unknown + } + + Write-Host "$logLead : Environment determined to be $serverEnvironment based on the $Global:AlkamiTagKeyEnvironment tag value" + return $serverEnvironment + } + + $environmentType = Get-AppSetting "Environment.Type" -ErrorAction SilentlyContinue + + # Get Environment from Features / Beacon Values if Tags Not Available. So long as it's an ORB server it should never really get past this stage + if (!([String]::IsNullOrEmpty($environmentType))) { + Write-Host "$logLead : Checking Environment.Type Value $environmentType from Machine Config Against Known List" + + $environment = switch ($environmentType) + { + "Production" { $Global:AlkamiTagValueEnvironmentProd } + "Staging" { $Global:AlkamiTagValueEnvironmentStaging } + "QA" { $Global:AlkamiTagValueEnvironmentQa } + "TeamQA" { $Global:AlkamiTagValueEnvironmentQa } + "Integration" { $Global:AlkamiTagValueEnvironmentQa } + "Development" { $Global:AlkamiTagValueEnvironmentDev } + } + + Write-Host "$logLead : Environment determined to be $environment based on machine.config" + return $environment + } + + # Get Environment from Computer Name if We Absolutely Have To. Some misconfigured CORP dev/qa environments might hit this + $compName = $env:ComputerName + if ($compName -like "ALK-*") { + if ($compName -like "*QA*") { + Write-Host ("$logLead : Environment determined to be {0} based on hostname {1}" -f $Global:AlkamiTagValueEnvironmentQa, $compName) + return $Global:AlkamiTagValueEnvironmentQa + } + else { + Write-Host ("$logLead : Environment determined to be {0} based on hostname {1}" -f $Global:AlkamiTagValueEnvironmentDev, $compName) + return $Global:AlkamiTagValueEnvironmentDev + } + } + + # Give up + Write-Warning ("$logLead : Unable to determine environment automatically") + return $unknown +} + diff --git a/Modules/Alkami.DevOps.Common/Public/Get-Environment.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-Environment.tests.ps1 new file mode 100644 index 0000000..cf44da4 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-Environment.tests.ps1 @@ -0,0 +1,113 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-Environment" { + + Context "Happy Path" { + + It "Returns the Expected Environment Based on AWS Tags" { + + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue; } + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentDev + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentQa + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentSandbox + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentSandbox + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentStaging + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentStaging + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentProd + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentProd + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentDR + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDR + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentLTM + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentLTM + + $Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentLoadtest + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentLoadtest + } + + It "Returns the Expected Environment Based on Machine Config Environment.Type Setting" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $Global:MockAppSettingValue; } + + $Global:MockAppSettingValue = "Development" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev + + $Global:MockAppSettingValue = "TeamQA" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa + + $Global:MockAppSettingValue = "QA" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa + + $Global:MockAppSettingValue = "Staging" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentStaging + + $Global:MockAppSettingValue = "Production" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentProd + } + + It "Returns the Expected Environment Based on (Shudder) Naming Convention" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null; } + + $env:ComputerName = "ALK-PLA1-QA999999999999" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa + + $env:ComputerName = "ALK-WIDGETSZ-WEB1" + Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev + } + } + + Context "Errors and Unknowns" { + + It "Returns Unknown if the AWS Tag Value is Unexpected" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue; } + + $Global:MockedEnvironmentValue = "You fool! You thought you could defeat me? With my power levels at OVER 50000?" + Get-Environment | Should -Be "Unknown" + } + + It "Returns Unknown if All Efforts At Finding a Value to Key Off of Fail" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null; } + $env:COMPUTERNAME = "WEB1234567" + + Get-Environment | Should -Be "Unknown" + } + + It "Returns Unknown if The $Global:AlkamiTagKeyEnvironment Tag Doesn't Exist" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $null; } + + Get-Environment | Should -Be "Unknown" + } + + It "Returns Unknown if The $Global:AlkamiTagKeyEnvironment Tag Is Empty" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return ""; } + + Get-Environment | Should -Be "Unknown" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-FileContentHash.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-FileContentHash.ps1 new file mode 100644 index 0000000..104fe29 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-FileContentHash.ps1 @@ -0,0 +1,29 @@ +function Get-FileContentHash { + <# + .SYNOPSIS + Retrieves a hash generated from a file's contents. + + .DESCRIPTION + Use this command to generate a hash for use in determining if a file's text contents have changed. + + .PARAMETER FilePath + [string] The full path (including file name) to the file to be hashed. Required. + + .EXAMPLE + Get-FileContentHash "C:\Temp\File.txt" + + .EXAMPLE + Get-FileContentHash -FilePath "C:\Temp\File.txt" +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias("FilePath")] + [string]$file + ) + + $fileContent = [System.IO.File]::ReadAllBytes($file) + $convertedContent = [System.Text.Encoding]::GetEncoding(1252).GetString($fileContent); + + return Get-UTF8ContentHash $convertedContent +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.ps1 new file mode 100644 index 0000000..7c6de0b --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.ps1 @@ -0,0 +1,115 @@ +function Get-HostnamesByEnvironmentName { + +<# + +.SYNOPSIS + Retrieves the hostnames for a specified environment. + +.PARAMETER EnvironmentName + [string] The moniker associated with the environment (e.g. 'Smith', '16.1') + +.PARAMETER EnvironmentType + [string] The type associated with the environment (e.g. 'prod', 'dr'). + +.PARAMETER IncludeOffline + [switch] Flag indicating whether or not to retrieve hostnames of offline instances. + +.PARAMETER ProfileName + [string] AWS profile name to use in the query. + +.PARAMETER Region + [string] AWS region to use in the query. + +.EXAMPLE + Get-HostnamesByEnvironmentName -EnvironmentName '16' -EnvironmentType 'DR' -Region 'us-west-2' -IncludeOffline -Verbose + +VERBOSE: [Get-HostnamesByEnvironmentName] : Using environment value 'DR' +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment DR +VERBOSE: [Get-HostnamesByEnvironmentName] : Designation value resolved to 'pod' +APP32101109 +APP326418 +MIC3210483 +MIC327179 +WEB3229114 +WEB323468 + +.EXAMPLE + Get-HostnamesByEnvironmentName -EnvironmentName 16 -Verbose + +VERBOSE: [Get-HostnamesByEnvironmentName] : Environment type not specified; attempting to determine value. +[Get-Environment] : Environment determined to be prod based on the alk:env tag value +VERBOSE: [Get-HostnamesByEnvironmentName] : Using environment value 'prod' +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prod +VERBOSE: [Get-HostnamesByEnvironmentName] : Designation value resolved to 'pod' +APP16107240 +APP1611088 +APP1612031 +APP16121149 +APP16122255 +MIC16106199 +MIC16108100 +MIC16117162 +MIC1612736 +MIC1665169 +WEB162253 +WEB1623247 +WEB163753 +WEB1647241 +WEB165798 +WEB166072 +#> + + [OutputType([string[]])] + [CmdletBinding()] + param( + [Parameter (Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$EnvironmentName, + + [Parameter (Mandatory = $false)] + [string]$EnvironmentType = $null, + + [Parameter(Mandatory = $false)] + [switch]$IncludeOffline, + + [Parameter(Mandatory = $false)] + [string]$ProfileName = $null, + + [Parameter(Mandatory = $false)] + [string]$Region = $null + ) + + $logLead = (Get-LogLeadName) + + if ( [string]::IsNullOrEmpty( $EnvironmentType ) ) { + Write-Verbose "$logLead : Environment type not specified; attempting to determine value." + [string] $EnvironmentType = ( Get-Environment ) + } + + Write-Verbose "$logLead : Using environment value '$EnvironmentType'" + + [string] $designation = ( Get-DesignationTagNameByEnvironment $environmentType ) + Write-Verbose "$logLead : Designation value resolved to '$designation'" + + $designationTag = ( "alk:{0}" -f $designation ) + + $searchTags = @{ + $designationTag = $EnvironmentName + $Global:AlkamiTagKeyEnvironment = $EnvironmentType.ToLowerInvariant() + } + + [Amazon.EC2.Model.Instance[]] $Instances = Get-InstancesByTag -tags $searchTags -IncludeOffline:$IncludeOffline -ProfileName $ProfileName -Region $Region + + [string[]] $hostNames = @() + foreach ( $instance in $instances ) { + + $hostname = Get-InstanceHostname $instance + + if ( ! [string]::IsNullOrEmpty( $hostname ) ) { + $hostNames += $hostname + } + } + + [Array]::Sort($hostNames) + return $hostNames +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.tests.ps1 new file mode 100644 index 0000000..473c0a6 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-HostnamesByEnvironmentName.tests.ps1 @@ -0,0 +1,164 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-HostnamesByEnvironmentName" { + + # Load up AWSPowerShell for Mocking if Available + $awsPowerShellLoaded = $false + if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) { + + Import-AWSModule # EC2 + $awsPowerShellLoaded = $true + } else { + + Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute." + } + + Context "Parameter Validation" { + + It "Null Environment Name Should Throw" { + + { Get-HostnamesByEnvironmentName -EnvironmentName $null } | Should Throw + } + + It "Empty Environment Name Should Throw" { + + { Get-HostnamesByEnvironmentName -EnvironmentName '' } | Should Throw + } + } + + Context "Get-Environment Is Called When EnvironmentType Parameter Is Null" { + Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() } + + $result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType $null ) + + It "Get-Environment Is Called One Time" { + + Assert-MockCalled Get-Environment -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + } + + Context "Get-Environment Is Called When EnvironmentType Parameter Is Empty" { + Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() } + + $result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType '' ) + + It "Get-Environment Is Called One Time" { + + Assert-MockCalled Get-Environment -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + } + + Context "Get-Environment Is Not Called When EnvironmentType Parameter Is Provided" { + Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() } + + $result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) + + It "Get-Environment Is Not Called" { + + Assert-MockCalled Get-Environment -Times 0 -Exactly -Scope Context -ModuleName $moduleForMock + } + } + + Context "Returns Empty Array When No Instances Are Found" { + Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() } + + It "Is Empty" { + + ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @() + } + } + + Context "Sorted Result Does Not Include Null Or Empty Entries" { + Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' } + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' } + + It "Does Not Include Null" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.InstanceId = 'Test' + + $nullInstance = (New-Object Amazon.EC2.Model.Instance) + $nullInstance.InstanceId = $null + + return @( $testInstance, $nullInstance ) + } + + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { + return $Instance.InstanceId.ToUpperInvariant() + } + + ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'TEST' ) + } + + It "Does Not Include Empty String" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.InstanceId = 'Test' + + $emptyInstance = (New-Object Amazon.EC2.Model.Instance) + $emptyInstance.InstanceId = '' + + return @( $testInstance, $emptyInstance ) + } + + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { + return $Instance.InstanceId.ToUpperInvariant() + } + + ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'TEST' ) + } + + It "Is Sorted" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { + $bInstance = (New-Object Amazon.EC2.Model.Instance) + $bInstance.InstanceId = 'B' + + $aInstance = (New-Object Amazon.EC2.Model.Instance) + $aInstance.InstanceId = 'A' + + return @( $bInstance, $aInstance ) + } + + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { + return $Instance.InstanceId.ToUpperInvariant() + } + + ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'A', 'B' ) + } + } +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-IPSTSUrlFromClient.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-IPSTSUrlFromClient.ps1 new file mode 100644 index 0000000..bdb3376 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-IPSTSUrlFromClient.ps1 @@ -0,0 +1,17 @@ +function Get-IPSTSUrlFromClient { +<# +.SYNOPSIS + Returns the IPSTS URL based on a Client Database object +#> + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = $true)] + [PSObject]$Client + ) + $queryString = "SELECT s.Value FROM core.ItemSetting s JOIN core.Item i on i.ID = s.ItemID JOIN core.Provider p on p.ID = i.ParentId WHERE REPLACE(p.AssemblyInfo, ' ', '') = " + + "'Alkami.Security.Provider.TokenTranslator.Entrust,Alkami.Security.Provider.TokenTranslator.Entrust.Provider' AND s.Name = 'Issuer'" + + return (Invoke-QueryOnClientDatabase $client $queryString) +} + + diff --git a/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.ps1 new file mode 100644 index 0000000..e52778e --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.ps1 @@ -0,0 +1,39 @@ +function Get-InstanceHostname { + +<# +.SYNOPSIS + Gets the hostname for an EC2 instance. + +.PARAMETER Instance + [Amazon.EC2.Model.Instance] The instance under inspection. + +.EXAMPLE + Get-InstanceHostname -Instance (Get-CurrentInstance) + +APP1611088 +#> + + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Amazon.EC2.Model.Instance]$Instance + ) + + $logLead = (Get-LogLeadName) + + [string] $hostname = ($instance.Tags | Where-Object { $_.Key -eq $Global:AlkamiTagKeyHostName } | Select-Object -First 1).Value + + if ( [string]::IsNullOrEmpty( $hostname ) ) { + + Write-Warning ( "{0} : No hostname found for {1}" -f $logLead, $instance.InstanceId ) + $hostname = $null + + } else { + + $hostname = $hostname.ToUpperInvariant() + } + + return $hostname +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.tests.ps1 new file mode 100644 index 0000000..6c6eb83 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-InstanceHostname.tests.ps1 @@ -0,0 +1,198 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-InstanceHostname" { + + # Load up AWSPowerShell for mocking if available + $awsPowerShellLoaded = $false + if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) { + + Import-AWSModule # EC2 + $awsPowerShellLoaded = $true + + } else { + + Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute." + } + + Context "Parameter Validation" { + + It "Null Instance Should Throw" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + { Get-InstanceHostname -Instance $null } | Should Throw + } + + It "Invalid Parameter Type Should Throw" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + { Get-InstanceHostname -Instance 'test' } | Should Throw + } + } + + Context "Hostname Determined By Instance Tag" { + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + It "Writes Warning When Hostname Tag Does Not Exist" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.InstanceId = 'Test' + + Get-InstanceHostname -Instance $testInstance | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" } + + } + + It "Returns Null When Hostname Tag Does Not Exist" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.InstanceId = 'Test' + + ( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull + } + + It "Writes Warning When Hostname Tag Is Null" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, $null)) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + Get-InstanceHostname -Instance $testInstance | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" } + + } + + It "Returns Null When Hostname Tag Is Null" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, $null)) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + ( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull + } + + It "Writes Warning When Hostname Tag Is Null" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, '')) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + Get-InstanceHostname -Instance $testInstance | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" } + + } + + It "Returns Null When Hostname Tag Is Null" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, '')) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + ( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull + } + + It "Does Not Write Warning When Hostname Tag Is Valid" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, 'Test')) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + Get-InstanceHostname -Instance $testInstance | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It ` + -ModuleName $moduleForMock + + } + + It "Returns Uppercased Hostname Tag Value When Hostname Tag Is Valid" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + $testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, 'Test')) + + $testInstance = (New-Object Amazon.EC2.Model.Instance) + $testInstance.Tags.Add($testTag) + $testInstance.InstanceId = 'Test' + + ( Get-InstanceHostname -Instance $testInstance ) | Should -BeExactly 'TEST' + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.ps1 new file mode 100644 index 0000000..8b860cd --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.ps1 @@ -0,0 +1,68 @@ +function Get-InstancesByTag { +<# +.SYNOPSIS + Gets EC2 Instances By Tag Filter + +.PARAMETER Tags + [hashtable] Mandatory. Keys are tag names, values are the value. + +.PARAMETER IncludeOffline + [switch] Include Instances not in a "running" state + +.PARAMETER ProfileName + [string] Specific AWS CLI Profile to use in AWS API calls + +.PARAMETER Region + [string] Specific AWS CLI Region to use in AWS API calls +#> + [CmdletBinding()] + param( + [Parameter (Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [hashtable]$Tags, + + [Parameter(Mandatory = $false)] + [switch]$IncludeOffline, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule # EC2 + + $splatParams = @{} + + $filters = @() + foreach ($key in $tags.Keys) { + $filter = (New-Object Amazon.EC2.Model.Filter); + $filter.Name = "tag:$key"; + $filter.Value = $tags.$key; + $filters += $filter; + } + + if (!($includeOffline.IsPresent)) { + $onlineFilter = (New-Object Amazon.EC2.Model.Filter) + $onlineFilter.Name = "instance-state-name" + $onlineFilter.Value = "running" + $filters += $onlineFilter; + } + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $splatParams["ProfileName"] = "$ProfileName" + } + + if (!([string]::IsNullOrEmpty($Region))) { + $splatParams["Region"] = "$Region" + } + + $instances = Get-EC2Instance -Filter $filters @splatParams + + Write-Verbose ("$logLead : Found {0} Instances with Filter" -f $instances.Count) + + return $instances.RunningInstance +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.tests.ps1 new file mode 100644 index 0000000..01597f7 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-InstancesByTag.tests.ps1 @@ -0,0 +1,67 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-InstancesByTag" { + + # Load up AWSPowerShell for Mocking if Available + $awsPowerShellLoaded = $false + if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) { + + Import-AWSModule # EC2 + $awsPowerShellLoaded = $true + } else { + + Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute." + } + + Context "Parameter and Environment Validation" { + + Mock -ModuleName $moduleForMock Get-EC2Instance { return @("I'm not Null!") } + + It "Excludes Offline Instances by Default" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + Get-InstancesByTag -tags @{"alk:pod" = "fakepod" } + Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { ($null -ne ($Filter | Where-Object { $_.Name -eq "instance-state-name" })) } + } + + It "Includes Offline Instances When Specified" { + + if (!($awsPowerShellLoaded)) { + + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + + # This test is less than ideal, since all we can realistically test is that the if statement which controls adding this filter or not + # is hit + Get-InstancesByTag -tags @{"alk:pod" = "fakepod" } -IncludeOffline + Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { ($null -eq ($Filter | Where-Object { $_.Name -eq "instance-state-name" })) } + } + + It "Correctly splats Region" { + Get-InstancesByTag -Tags @{"alk:pod" = "fakepod"} -Region "us-fake-1" + Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter {$Region -eq "us-fake-1"} + + } + It "Correctly splats Profile" { + Get-InstancesByTag -Tags @{"alk:pod" = "fakepod"} -ProfileName "fake-profile" + Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter {$ProfileName -eq "fake-profile"} + + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.ps1 new file mode 100644 index 0000000..07051da --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.ps1 @@ -0,0 +1,91 @@ +function Get-LocalNlbIp { + +<# +.SYNOPSIS + Gets the Ip for the NLB NIC which is in the same AZ as the server from which it's run. + +.DESCRIPTION + Gets the Ip for the NLB NIC which is in the same AZ as the server from which it's run. Uses the current availability zone, ENI description, and interfacetype to determine the appropriate IP + +.EXAMPLE + Get-LocalNlbIp -verbose + +VERBOSE: [Get-LocalNlbIp] : Current Instance AZ Read as us-east-1b +VERBOSE: [Get-LocalNlbIp] : Environment Read as qa +[Get-DesignationTagNameByEnvironment] : Checking designation value for environment qa +VERBOSE: [Get-LocalNlbIp] : Read designation tag value Smith +VERBOSE: [Get-LocalNlbIp] : Using Expected NLB Name ELB net/Smith-qa-nlb for Filtering +VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeNetworkInterfaces' in region 'us-east-1' +Returning IP Address for ENI with Description: ELB net/smith-qa-nlb/93947386b64a5aac, Id: eni-0718dc98cdcec5e18 +10.26.91.212 +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName) + + Import-AWSModule # EC2 + + if (!(Test-IsAws)) + { + Write-Warning "$logLead : This function can only be executed on an AWS server" + return + } + + # Get the current instance and AZ + $currentInstance = Get-CurrentInstance; + $currentAz = $currentInstance.Placement.AvailabilityZone; + Write-Verbose "$logLead : Current Instance AZ Read as $currentAz" + + # Check the current server's role + $serverRole = $currentInstance.Tag | Where-Object {$_.Key -eq $Global:AlkamiTagKeyRole} + if ($serverRole.Value -eq 'app:app') + { + # App servers should use 127.0.0.1 + Write-Warning "This is currently running on an app server. The IP returned shouldn't be used in the host file." + } + + # Get the expected designation tag name + $environment = $currentInstance.Tag | Where-Object { $_.Key -eq $Global:AlkamiTagKeyEnvironment; }; + Write-Verbose "$logLead : Environment Read as $($environment.Value)" + + $targetTag = Get-DesignationTagNameByEnvironment $environment.Value + + if ($null -ne $targetTag) { + + # Pull the Designation Tag Value + $environmentTagValue = $currentInstance.Tag | Where-Object {$_.Key -eq "alk:$targetTag" } + Write-Verbose "$logLead : Read designation tag value $($environmentTagValue.Value)" + + } else { + + Write-Warning "$logLead : Unable to pull $Global:AlkamiTagKeyEnvironment for the current instance. Execution cannot continue." + return $null; + } + + $cleanedName = $environmentTagValue.Value.replace('.','-'); + $nlbName = "ELB net/" + $cleanedName + '-' + $environment.Value + '-nlb'; + Write-Verbose "$logLead : Using Expected NLB Name $nlbName for Filtering" + + $nlbNics = Get-EC2NetworkInterface -Filter @( @{name='availability-zone';values=$currentAz} ); + [array]$filteredNics = $nlbNics | Where-Object { $_.InterfaceType -eq 'network_load_balancer' -and $_.Description -match $nlbName} + $matchCount = $filteredNics.Count + Write-Verbose "$logLead : Found $matchCount Matching ENIs with InterfaceType: network_load_balancer, Description: $nlbName, Availability Zone $currentAz" + + if ($null -ne $filteredNics -and $filteredNics.Count -eq 1) { + + $nic = $filteredNics | Select-Object -First 1 + Write-Host ("Returning IP Address for ENI with Description: {0}, Id: {1}" -f $nic.Description, $nic.NetworkInterfaceId) + return (($nic | Select-Object -First 1).PrivateIpAddress); + } + + if ($null -eq $filteredNics) { + + Write-Warning "$logLead : No ENIs found with Description $nlbName for AZ $currentAz" + return $null + } + + Write-Warning ("$logLead : {0} ENIs found with Description $nlbName for AZ $currentAz. Execution cannot continue." -f $filteredNics.Count) + return $null +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.tests.ps1 new file mode 100644 index 0000000..95b924d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-LocalNlbIp.tests.ps1 @@ -0,0 +1,329 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-LocalNlbIp + +# Handle the AWS Native Functions When AWSPowerShell Not Available +if ($null -eq (Get-Command "Get-EC2NetworkInterface" -ErrorAction SilentlyContinue)) +{ + function Get-EC2NetworkInterface { + + throw "This Should Never Be Actually Called" + } +} + +Describe "Get-LocalNlbIp" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { return $null } + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-1a"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentProd}, + @{Key = "alk:pod"; Value = "6"} + ); + } + + return $pod6Instance + } + + Context "When Environment Names Overlap" { + + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { + + # args[1] is an ec2 filter based on the AvailabilityZone from the current instance + # https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html + $ec2Filter = $args[1] + + Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)" + + $possibleReturns = @( + + @{ "Description"="ELB net/12.6-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/6-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"} + ) + + return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values} + } + + It "Returns the Correct IP When Searching for POD 6 and 12.6 is Available" { + + Get-LocalNlbIp | Should -Be "100.100.100.100" + } + } + + Context "When the Environment is an App Server" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-1a"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleApp}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentProd}, + @{Key = "alk:pod"; Value = "6"} + ); + } + + return $pod6Instance + } + + It "Writes a Warning" { + + ( Get-LocalNlbIp 3>&1 ) -match "This is currently running on an app server." | Should -Be $true + } + } + + Context "Environment Tag Switch" { + + $validEnvironments = @("Prod", "DR") + + foreach ($Global:podBasedEnvironment in $validEnvironments) { + + It "Uses the alk:pod tag when the environment is $podBasedEnvironment" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-1a"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"}, + @{Key = "alk:pod"; Value = "Pass"}, + @{Key = "alk:lane"; Value = "Fail"} + ); + } + + return $pod6Instance + } + + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { + + # args[1] is an ec2 filter based on the NLB name constructed from the current instance + # https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html + $ec2Filter = $args[1] + + Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)" + + $possibleReturns = @( + + @{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/Fail-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/Fail-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"} + ) + + return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values} + } + + Get-LocalNlbIp | Should -Be 100.100.100.100 + } + } + + It "Uses the alk:lane tag when the environment is Staging" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-1a"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentStaging}, + @{Key = "alk:pod"; Value = "Fail"}, + @{Key = "alk:lane"; Value = "Pass"} + ); + } + + return $pod6Instance + } + + + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { + + # args[1] is an ec2 filter based on the NLB name constructed from the current instance + # https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html + $ec2Filter = $args[1] + + Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)" + + $possibleReturns = @( + + @{ "Description"="ELB net/Pass-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/Fail-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"} + @{ "Description"="ELB net/Fail-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"} + ) + + return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values} + } + + Get-LocalNlbIp | Should -Be 100.100.100.100 + } + + $validEnvironments = @("Dev", "QA") + + foreach ($Global:designationBasedEnvironment in $validEnvironments) { + It "Uses the alk:designation tag when the environment is $designationBasedEnvironment" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + "AvailabilityZone" = "us-east-1a"; + } + + $instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = "$designationBasedEnvironment"}, + @{Key = "alk:pod"; Value = "Fail"}, + @{Key = "alk:lane"; Value = "Fail"}, + @{Key = "alk:designation"; Value = "Pass"} + ); + } + + return $instance + } + + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { + + # args[1] is an ec2 filter based on the NLB name constructed from the current instance + # https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html + $ec2Filter = $args[1] + + Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)" + + $possibleReturns = @( + @{ "Description"="ELB net/Pass-$designationBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/Fail-$designationBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"} + @{ "Description"="ELB net/Fail-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"} + ) + + return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values} + } + + Get-LocalNlbIp | Should -Be 100.100.100.100 + } + } + } + + Context "Error Scenarios" { + + Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { + + # args[1] is an ec2 filter based on the NLB name constructed from the current instance + # https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html + $ec2Filter = $args[1] + + Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)" + + $possibleReturns = @( + + @{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"}, + @{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="111.111.111.111"; InterfaceType="network_load_balancer"} + ) + + return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values} + } + + It "Writes a Warning and Returns Null When No Match Found" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-999"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"}, + @{Key = "alk:pod"; Value = "Pass"}, + @{Key = "alk:lane"; Value = "Fail"} + ); + } + + return $pod6Instance + } + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + Get-LocalNlbIp | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "No ENIs found with Description" } + } + + It "Writes a Warning and Returns Null When More Than One Match Found" { + + Mock -ModuleName $moduleForMock Get-CurrentInstance { + + $placementMock = New-Object PSObject -Property @{ + + "AvailabilityZone" = "us-east-1a"; + } + + $pod6Instance = New-Object PSObject -Property @{ + + Placement = $placementMock; + + Tag = @( + @{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb}, + @{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"}, + @{Key = "alk:pod"; Value = "Pass"}, + @{Key = "alk:lane"; Value = "Fail"} + ); + } + + return $pod6Instance + } + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + Get-LocalNlbIp | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "2 ENIs found with Description" } + } + } +} + +#endregion Get-LocalNlbIp \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-LogDiskUtilization.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-LogDiskUtilization.ps1 new file mode 100644 index 0000000..d5427d4 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-LogDiskUtilization.ps1 @@ -0,0 +1,109 @@ +function Get-LogDiskUtilization { + +<# +.SYNOPSIS + Gets disk utilization from ORB Logs + +.PARAMETER ComputerName + The single remote computer name to execute against. If not supplied, runs on the local system +#> + + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = "" + ) + + $logLead = Get-LogLeadName + $isLocalExecution = $false + + if (( [String]::IsNullOrEmpty($ComputerName) ) -or (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName )) { + + Write-Verbose "$logLead : Setting local execution to true" + $ComputerName = $ENV:ComputerName + $isLocalExecution = $true + } + + $logPath = Get-ORBLogsPath + if ($isLocalExecution) { + + Write-Verbose "$logLead : Searching for log files locally" + $logFiles = Get-ChildItem -Path $logPath -Filter *.log* -File + } else { + + Write-Verbose "$logLead : Searching for log files on $ComputerName" + $logFiles = Invoke-Command $ComputerName { Get-ChildItem -Path $using:logPath -Filter *.log* -File } + } + + if (Test-IsCollectionNullOrEmpty $logFiles) { + + Write-Host "$logLead : No log files found under $logPath on computer $ComputerName" + return + } + + $logDetails = @() + foreach ($logFile in $logFiles) { + $logDetails += New-Object PSObject -Property @{ + + Name = $logFile.BaseName -replace ".log$", ""; + Size = $logFile.Length + } + } + + $groups = $logDetails | Group-Object -Property Name + $totalDiskUtilization = 0 + + $analysisResults = @() + foreach ($logGroup in ($groups | Sort-Object -Property Count -Descending)) { + + [array]$sizeDetails = $logGroup.Group | Select-Object -ExpandProperty Size + if ($sizeDetails.Count -eq 0) { + $cumulativeSize = $sizeDetails; + } else { + + [int]$cumulativeSize = 0 + $sizeDetails | ForEach-Object { + $cumulativeSize += [int]$_ + } + } + + $totalDiskUtilization += $cumulativeSize + $analysisResults += New-Object PSObject -Property @{ + + LogFile = $logGroup.Name; + LogCount = $logGroup.Count; + Size = $cumulativeSize; + } + } + + $topTenLogSizes = $analysisResults | Sort-Object -Property Size -Descending | Select-Object -First 10 + + # Begin unnecessarily complicated table print that I threw together during a meeting because I was bored + $maxLogNameLength = ($topTenLogSizes | Select-Object -ExpandProperty LogFile | Sort-Object -Property Length -Descending | Select-Object -First 1).Length + $maxLogNamePadding = [math]::Max($maxLogNameLength, 8) + + $maxLogSizeLength = ($topTenLogSizes | Select-Object -ExpandProperty Size | ForEach-Object { $_.ToString() } | Sort-Object -Property Length -Descending | Select-Object -First 1).Length + $logSizePadding = [math]::Max($maxLogSizeLength , 4) + + $headerRow = (" | " + ("Log File".PadRight($maxLogNameLength), "Size".PadRight($maxLogSizeLength), "Log Count" -Join " | ") + " | ") + $headerRowLength = $headerRow.Length - 4 + + $totalUtilizationLine = " | $ComputerName Log Utilization: $([math]::Round(($totalDiskUtilization/1024/1024),2))mb".PadRight($headerRowLength + 2) + "|" + + $border = " %" + ("=" * $headerRowLength) + "%" + + Write-Host $border + Write-Host $totalUtilizationLine + Write-Host $border + Write-Host $headerRow + Write-Host $border + + foreach ($topLog in $topTenLogSizes) { + + $outputLine = " | " + ($topLog.LogFile.PadRight($maxLogNamePadding), $topLog.Size.ToString().PadRight($logSizePadding), $topLog.LogCount.ToString().PadRight(9) -Join " | ") + " | " + Write-Host $outputLine + } + + Write-Host $border +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-PackageForInstallation.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-PackageForInstallation.ps1 new file mode 100644 index 0000000..75f582b --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-PackageForInstallation.ps1 @@ -0,0 +1,39 @@ +function Get-PackageForInstallation { +<# +.SYNOPSIS + Downloads a File to the Specified Outputfolder +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$loglead, + + [Parameter(Mandatory = $true)] + [string]$outputFolder, + + [Parameter(Mandatory = $true)] + [string]$outputfileName, + + [Parameter(Mandatory = $true)] + [string]$downloadUrl + ) + + if (!(Test-Path $outputFolder)) { + Write-Verbose ("$logLead : Creating Package Download Folder {0}" -f $outputFolder) + New-Item $outputFolder -ItemType Directory | Out-Null + } + + $downloadFile = (Join-Path $outputFolder $outputfileName) + + if (Test-Path $downloadFile) { + # If a job is rerun, this should prevent downloading again, unless it spans a day + Write-Host ("$logLead : Using Existing Package from {0}" -f $downloadFile) + } + else { + Write-Host ("$logLead : Downloading File from {0} to {1}" -f $downloadUrl, $downloadFile) + Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadFile + } + + return $downloadFile +} diff --git a/Modules/Alkami.DevOps.Common/Public/Get-PodName.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-PodName.ps1 new file mode 100644 index 0000000..d3a2b59 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-PodName.ps1 @@ -0,0 +1,39 @@ +function Get-PodName { + <# + .SYNOPSIS + Easily return human-readable name for the pod, lane, designation in which a host belongs. + .DESCRIPTION + Query the host's Machine.config to determine the pod, lane, designation, etc, and return in a format people can use. + + .PARAMETER ComputerName + Optional Parameter. If specified, gets the name for that server's pod, lane, designation, etc. + + .PARAMETER Full + Optional Parameter. If specified, returns the full name of the environment. Otherwise, returns the short name. + + .EXAMPLE + + Get-PodName -ComputerName APP169671 + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = "$env:computerName", + [Parameter(Mandatory = $false)] + [switch]$Full + ) + + # get the FQDN no matter what for simplicity + $ComputerName=[System.Net.Dns]::GetHostByName($ComputerName).HostName + + # get the key value + $keyValue = Get-AppSetting -Key Environment.Name -ComputerName $ComputerName + + if ( $Full ) { + $keyValue + } else { + $podName = ($keyValue -split " ")[-1] + $podName + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SecretsForPod.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SecretsForPod.ps1 new file mode 100644 index 0000000..2be196d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SecretsForPod.ps1 @@ -0,0 +1,50 @@ +function Get-SecretsForPod { +<# +.SYNOPSIS + Gets Secrets for Pod. +#> + + [CmdletBinding()] + Param( + [string]$secretUserName, + [string]$secretPassword, + [string]$secretDomain, + [string]$secretFolderNames + ) + + $client = New-Object Alkami.Ops.SecretServer.Client + $authResult = $client.AuthenticateAsync($secretUserName, $secretPassword, $secretDomain).GetAwaiter().GetResult() + + if ($authResult.Status -ne [Alkami.Ops.SecretServer.Enum.ResultStatus]::Success) { + Write-Warning ("Unable to authenticate with SecretServer: {0}" -f $authResult.Message) + + if ($authResult.Errors.Count -gt 0) { + $errors = ("Error(s) from server - " + ($authResult.Errors | Select-Object -ExpandProperty "ErrorMessage") -join ", ") + Write-Warning -Message $errors + } + + return + } + + [HashTable]$secrets = $null + + foreach ($secretFolder in $secretFolderNames.Split(',')) { + $result = $client.GetFolderSecretsAsync($secretFolder.Trim()).GetAwaiter().GetResult() + + if ($result.Status -ne [Alkami.Ops.SecretServer.Enum.ResultStatus]::Success) { + Write-Warning ("Error pulling secrets for {0} from SecretServer: {1}" -f $secretFolder, $secretResult.Message) + + if ($secretResult.Errors.Count -gt 0) { + $errors = ("Error(s) from server - " + ($secretResult.Errors | Select-Object -ExpandProperty "ErrorMessage") -join ", ") + Write-Warning -Message $errors + } + + return + } + + $secrets += $result.Secrets + } + + return $secrets +} + diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SecurityGroupPrefix.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SecurityGroupPrefix.ps1 new file mode 100644 index 0000000..a983684 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SecurityGroupPrefix.ps1 @@ -0,0 +1,28 @@ +function Get-SecurityGroupPrefix { + <# + .SYNOPSIS + Easily return human-readable name for the Security Group in which a host belongs. + .DESCRIPTION + Query the host's Machine.config to determine the Security Group for a host and return in a format people can use. + + .PARAMETER ComputerName + Optional Parameter. If specified, gets the Security Group for that server's pod, lane, designation, etc. Defaults to local computer. + + .EXAMPLE + + Get-SecurityGroup -ComputerName APP169671 + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName="$env:computerName" + ) + + # get the FQDN no matter what for simplicity + $ComputerName=[System.Net.Dns]::GetHostByName($ComputerName).HostName + + $securityGroup=Get-AppSetting -Key Environment.UserPrefix -ComputerName $ComputerName + + return $securityGroup +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-ServerStatusReport.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-ServerStatusReport.ps1 new file mode 100644 index 0000000..53fabc5 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-ServerStatusReport.ps1 @@ -0,0 +1,166 @@ +function Get-ServerStatusReport { +<# +.SYNOPSIS +Start the Server Status check, typically run after a Scale-Up event. + +.DESCRIPTION +Performs several sanity tests to ensure the servers are fully operational after a Scale-Up event. +Main entrypoint that calls this is in teamcity.sre.code/ScaleEnvironments/Invoke-ReportServerStatus.ps1 + +.PARAMETER Servers +[string[]]Array of servers to run tests against. Usually this is a list of servers by Designation (LC3, Morph, etc.) + +.PARAMETER ProfileName +[string] Specific AWS CLI Profile to use in AWS API calls. + +.PARAMETER Region +[string] Specific AWS CLI Region to use in AWS API calls. + +.NOTES +Outputs a table with the results of the test by host. +Returns custom object with the following properties: + "Hostname" + "Designation" + "InLoadBalancer" + "IsNagRunning" + "IsSubServiceRunning" + "AllAlkServicesRunning" + "TagsCorrect" + "IsAppServer" + "IsWebServer" + "IsMicServer" +#> + [CmdletBinding()] + [OutputType([Object])] + Param ( + [Parameter(Mandatory = $true)] + [string[]] $Servers, + + [Parameter(Mandatory = $true)] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [string] $Region + ) + + $logLead = (Get-LogLeadName) + + Write-Host "$logLead : Running for servers:`n$Servers" + + # Script Block to do the tests. + $testsScriptBlock = { + param ($hostname, $arguments) + + Write-Host "##teamcity[blockOpened name='Running tests for hostname $hostname']" + + $testResults = Invoke-Command -ComputerName $hostname -ScriptBlock { + $computername = (Get-FullyQualifiedServerName) + $isAppServer = Test-IsAppServer -ComputerName $computername + $isWebServer = Test-IsWebServer -ComputerName $computername + $isMicServer = Test-IsMicroServer -ComputerName $computername + + # Make an object to hold the results of the tests + $testResult = New-Object PSObject -Property @{ + "Hostname" = $computername + "Designation" = "Unknown" + "InLoadBalancer" = $null + "IsNagRunning" = $null + "IsSubServiceRunning" = $null + "AllAlkServicesRunning" = $null + "TagsCorrect" = $null + "IsAppServer" = $isAppServer + "IsWebServer" = $isWebServer + "IsMicServer" = $isMicServer + } + + # NAG test + try { + if($IsAppServer) { + Write-Host "Running NAG test" + $nagStatus = Test-IsNagRunning -Server $computername 2>$null + $testResult.IsNagRunning = $nagStatus + } else { + $testResult.IsNagRunning = "N/A" + } + } catch { + Resolve-Error -ErrorRecord $_ + $testResult.IsNagRunning = "Unknown" + } + + + # Subscription Service test + try { + if($isMicServer -or $isWebServer -or $isAppServer) { + Write-Host "Running Subscription Service test" + $chocoServices = Get-ChocolateyServices 2>$null + $subscriptionService = ($chocoServices | Where-Object {$_.Name -eq "Alkami.Services.Subscriptions.Host"}) + if($null -ne $subscriptionService) { + $testResult.IsSubServiceRunning = ($subscriptionService.State -eq "Running") + } else { + $testResult.IsSubServiceRunning = $false + } + } else { + $testResult.IsSubServiceRunning = "N/A" + } + } catch { + Resolve-Error -ErrorRecord $_ + $testResult.IsSubServiceRunning = "Unknown" + } + + + # All ALK Services running test? + Write-Host "Running All Alk Services test" + $alkServices = ($chocoServices | Where-Object {$_.Name -like "Alkami.*"}) + $stoppedAlkServiceCount = ($alkServices | Where-Object {$_.State -eq "Stopped"}).count + $testResult.AllAlkServicesRunning = !($stoppedAlkServiceCount -gt 0) + + + # Tags test + try { + Write-Host "Running Tags test" + $tags = Get-CurrentInstanceTags + $autoShutdownTagKey = "alk:autoshutdown" + $autoShutdownTag = ($tags | Where-Object { ($_.Key -eq $autoShutdownTagKey -and $_.Value -eq "true") }) + $isAutoShutdown = ($null -ne $autoShutdownTag) + $testResult.TagsCorrect = (!($isAutoShutdown)) + } catch { + Resolve-Error -ErrorRecord $_ + $testResult.TagsCorrect = "Unknown" + } + + return $testResult + } + + # LoadBalancer test + # This test is done outside of the above script block as it needs to be run off-host. + # Running this on host will fail due to the IAM creds not having permissions for some + # resources (Only for NGinx lookups). + try { + if($testResults.IsAppServer -or $testResults.IsWebServer) { + Write-Host "Running LoadBalancer test" + $lbState = Get-LoadBalancerState -Server $hostname -AwsProfileName $arguments.ProfileName -AwsRegion $arguments.Region + $testResults.InLoadBalancer = ($lbState -eq "Active") + } else { + $testResults.InLoadBalancer = "N/A" + } + } catch { + Resolve-Error -ErrorRecord $_ + $testResults.InLoadBalancer = "Unknown" + } + + Write-Host "##teamcity[blockClosed name='Running tests for hostname $hostname']" + + # Return the test + return $testResults + } + + # Parallel over the servers to test. + try { + $results = (Invoke-Parallel2 -Objects $Servers -Arguments @{ProfileName = $ProfileName; Region = $Region;} -Script $testsScriptBlock).Result + } catch { + Resolve-Error -ErrorRecord $_ + } + + # Send results to caller. + return $results +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SlackAction.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SlackAction.ps1 new file mode 100644 index 0000000..5e5a161 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SlackAction.ps1 @@ -0,0 +1,34 @@ +function Get-SlackAction { +<# +.SYNOPSIS + Used to create interactive elements in messages + +.PARAMETER Text + The text to display + +.PARAMETER Url + The URL the user will interact with + +.PARAMETER Type + Defaults to button +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true)] + [string]$Text, + + [Parameter(Mandatory = $true)] + [string]$Url, + + [Parameter(Mandatory = $false)] + [ValidateSet('button')] # We only usually offer one option type. Feel free to update when you've got a new use-case + [string]$Type = 'button' + ) + + return @{ + text = $Text + url = $Url + type = $Type + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachment.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachment.ps1 new file mode 100644 index 0000000..b7f6af5 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachment.ps1 @@ -0,0 +1,87 @@ +function Get-SlackAttachment { +<# +.SYNOPSIS + Used to create an attachment for a Slack Message that adds further context or additional information. + +.DESCRIPTION + Content that can be attached to messages to include lower priority content - content that doesn't necessarily need to be seen to appreciate the intent of the message. + +.PARAMETER Fallback + A plain text summary of the attachment used in clients that don't show formatted text (eg. IRC, mobile notifications). + The top-level text field from the message payload. + +.PARAMETER Color + The color showcased in the message to show status. + +.PARAMETER Fields + An array of field objects that get displayed in a table-like way. For best results, include no more than 2-3 field objects. + +.PARAMETER Text + The main body text of the attachment. + +.PARAMETER Timestamp + The attachment will display the additional timestamp value as part of the attachment's footer. + Your message's timestamp will be displayed in varying ways, depending on how far in the past or future it is, relative to the present. Form factors, like mobile versus desktop may also transform its rendered appearance. + +.PARAMETER Actions + A set of actions to be taken for this attachment. + +.PARAMETER ActionText + The text for an attachment. + +.PARAMETER ActionUrl + The URL for an attachment. +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true)] + [string]$Fallback, + + [Parameter(Mandatory = $false)] + [string]$Color = $null, + + [Parameter(Mandatory = $false)] + [string]$Text = $null, + + [Parameter(Mandatory = $false)] + [DateTime]$Timestamp = (Get-Date), + + [Parameter(Mandatory = $false)] + [object[]]$Fields = $null, + + [Parameter(ParameterSetName = 'Actions')] + [object[]]$Actions = $null, + + [Parameter(Mandatory = $true, ParameterSetName = 'ActionPair')] + [string]$ActionText, + + [Parameter(Mandatory = $true, ParameterSetName = 'ActionPair')] + [string]$ActionUrl + ) + + $messageColor = Get-SlackMessageColor -Text $Color + Write-Verbose "Message Color: $messageColor" + $attachment = @{ + fallback = $Fallback + color = $messageColor + ts = [Math]::Floor([decimal](Get-Date($Timestamp).ToUniversalTime() -uformat "%s")) + } + + if ($null -ne $Fields) { + $attachment.fields = $Fields + } + + if (![string]::IsNullOrWhiteSpace($Text)) { + $attachment.text = $Text + } + + if ($PSCmdlet.ParameterSetName -eq 'ActionPair') { + $Actions = Get-SlackAction -Text $ActionText -Url $ActionUrl + } + + if ($null -ne $Actions) { + $attachment.actions = @($Actions) + } + return $attachment +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachmentFields.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachmentFields.ps1 new file mode 100644 index 0000000..08eb512 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SlackAttachmentFields.ps1 @@ -0,0 +1,35 @@ +function Get-SlackAttachmentFields { +<# +.SYNOPSIS + Used to create a Field object for the Slack Attachment that attaches to the Slack Message. + +.PARAMETER Title + Shown as a bold heading displayed in the field object. + +.PARAMETER Value + The text value displayed in the field object. + +.PARAMETER Short + Indicates whether the field object is short enough to be displayed side-by-side with other field objects. Defaults to false. +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $false)] + [string]$Title = "", + + [Parameter(Mandatory = $false)] + [string]$Value = "", + + [Parameter(Mandatory = $false)] + [switch]$Short + ) + + $result = @{ + title = $Title + value = $Value + short = $Short + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SlackMessage.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SlackMessage.ps1 new file mode 100644 index 0000000..33d5050 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SlackMessage.ps1 @@ -0,0 +1,59 @@ +function Get-SlackMessage { +<# +.SYNOPSIS + Create a Slack Message. + +.PARAMETER Username + To specify the username for the published message. + +.PARAMETER IconEmoji + To specify an emoji (using colon shortcodes, eg. :white_check_mark:) to use as the profile photo alongside the message. + +.PARAMETER Text + The main body text of the message. + +.PARAMETER Channel + Channel, private group, or IM channel to send message to. + +.PARAMETER Attachments + One of these arguments is required to describe the content of the message. See the function 'Get-SlackAttachment' for more information. +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true)] + [string]$Username, + + [Parameter(Mandatory = $true)] + [string]$IconEmoji, + + [Parameter(Mandatory = $true)] + [string]$Text, + + [Parameter(Mandatory = $true)] + [string]$Channel, + + [Parameter(Mandatory = $false)] + [object[]]$Attachments = $null + ) + + $IconEmoji = $IconEmoji.replace(':', '') + $normalizedIconEmoji = ":$($IconEmoji):" + + $message = @{ + username = $Username + icon_emoji = $normalizedIconEmoji + text = $Text + channel = $Channel + } + + if (![string]::IsNullOrWhiteSpace($Channel)) { + $message.channel = $Channel + } + + if ($null -ne $Attachments) { + $message.attachments = @($Attachments) + } + + return $message +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-SlackMessageColor.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-SlackMessageColor.ps1 new file mode 100644 index 0000000..f54954d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-SlackMessageColor.ps1 @@ -0,0 +1,68 @@ +function Get-SlackMessageColor { +<# +.SYNOPSIS + Provide a standardized way to get slack channel colors + This way we use a consistent color set, etc + +.PARAMETER Success + Will return a green color + +.PARAMETER Failure + Will return a red color + +.PARAMETER Unknown + Will return a blue color + +.PARAMETER Text + Will attempt to find a suitable color to match + Can also be provided a known hex value, which will be returned again (helpful for pass-through cases from prior function calls) + Will return blue if the input is neither "success" nor "failure" (until we add more cases) +#> + [CmdletBinding(DefaultParameterSetName = 'text')] + [OutputType([string])] + param ( + [Parameter(ParameterSetName = 'success')] + [switch]$Success, + + [Parameter(ParameterSetName = 'failure')] + [Alias('Fail')] + [switch]$Failure, + + [Parameter(ParameterSetName = 'unknown')] + [switch]$Unknown, + + [Parameter(Mandatory = $false, ParameterSetName = 'text')] + [string]$Text = "" + ) + + $messageColors = @{ + SUCCESS = "#119e42" + FAILURE = "#cc2614" + DEFAULT = "#0000ff" + } + + # Early bail, PS does not support eager evaluation, so nest + if (![string]::IsNullOrWhiteSpace($Text)) { + if ($Text.StartsWith('#')) { + return $Text + } + } + + if ($Success) { + $Text = 'Success' + } + + if ($Failure) { + $Text = 'Failure' + } + + if ($Unknown) { + $Text = 'Unknown' + } + + switch ($Text) { + 'Success' { $messageColors.SUCCESS } + 'Failure' { $messageColors.FAILURE } + default { $messageColors.DEFAULT } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-UTF8ContentHash.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-UTF8ContentHash.ps1 new file mode 100644 index 0000000..7f1947c --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-UTF8ContentHash.ps1 @@ -0,0 +1,34 @@ +function Get-UTF8ContentHash { + <# + .SYNOPSIS + Retrieves a hash generated from a UTF8 string. + + .PARAMETER UTF8Content + [string] The UTF8 string to be hashed. Required. + + .EXAMPLE + Get-UTF8ContentHash "Test" + + .EXAMPLE + $myString = "Test" + Get-UTF8ContentHash -UTF8Content $myString +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias("UTF8Content")] + [string]$content + ) + + $encoding = New-Object System.Text.UTF8Encoding($false) + $bytes = $encoding.GetBytes($content) + $hashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create("SHA256") + $hash = $hashAlgorithm.ComputeHash($bytes) + + $builder = New-Object -TypeName "System.Text.StringBuilder" + foreach ($byte in $hash) { + $builder.Append($byte.ToString("x2")) | Out-Null + } + + return $builder.ToString() +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Get-UserCredentialsFromSecretServer.ps1 b/Modules/Alkami.DevOps.Common/Public/Get-UserCredentialsFromSecretServer.ps1 new file mode 100644 index 0000000..78fb3f3 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Get-UserCredentialsFromSecretServer.ps1 @@ -0,0 +1,122 @@ +function Get-UserCredentialsFromSecretServer () { +<# +.SYNOPSIS + Gets the username and password of a user secret from Secret Server. + +.PARAMETER secretCredential + Credentials of the user to authenticate with on Secret Server. + +.PARAMETER folderName + The name of the folder the secret is in on Secret Server. + +.PARAMETER secretName + The name of the secret on Secret Server + +.OUTPUTS + Either a credential object containing the username and password of the user or null. + +.EXAMPLE + Get-UserCredentialsFromSecretServer -secretCredential $credential -folderName "Entrust 17-0" -secretName "Entrust 17-0 Master1 User" + +Password Username +-------- -------- +SecretStuffs Master1 +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [PSCredential]$secretCredential, + [Parameter(Mandatory=$true)] + [String]$folderName, + [Parameter(Mandatory=$true)] + [String]$secretName + ) + + $loglead = (Get-LogLeadName) + + $secretServerUri = (Get-SecretServerUri) + + # TODO: If the low-level function accepts credential objects, this can be simplified. + Write-Verbose "$loglead : Creating Secret Server Connection to $secretServerUri with UserName $($secretCredential.UserName)" + $secretServer = Get-SecretServerConnection $secretServerUri $secretCredential.UserName (Get-PasswordFromCredential $secretCredential) + + Write-Verbose "$loglead : Authenticating to SecretServer with No 2FA" + $secretServer.Authenticate($false) + + Write-Verbose "$loglead : Searching for folder named '$folderName'" + [array]$folderIds = $secretServer.GetFolderIdByName($folderName) + if ( Test-IsCollectionNullOrEmpty $folderIds ) { + Write-Error "$logLead : Could not find a match for folder name '$folderName'. Returning null" + return $null + } + + foreach ($folderIdentifier in $folderIds) { + + # This will return an array of secret records + Write-Host "$loglead Searching for secrets with name '$secretName' in folder ID '$folderIdentifier'" + $matchingSecrets = $secretServer.GetSecretByName($secretName, $folderIdentifier) + + $potentialSecretsCount = $matchingSecrets.Records.Count + if($null -eq $matchingSecrets -or $potentialSecretsCount -eq 0) { + + if ($folderIds.Count -eq 1) { + + Write-Error "$loglead : Could not find secrets matching derived secret name '$secretName' in folder with ID $folderIdentifier" + return $null + } + + Write-Host "$loglead : Could not find secrets matching derived secret name '$secretName' in folder with ID $folderIdentifier. Continuing..." + continue + } + + if ($potentialSecretsCount -gt 1) { + + # We found more than one secret. Look for an exact name match + Write-Verbose "$logLead : Found $potentialSecretsCount Secrets Matching Search String '$secretName'." + + foreach ($potentialSecret in $matchingSecrets.Records) { + + if ($potentialSecret.name -eq $secretName) { + + Write-Host "$logLead : Secret with ID $($potentialSecret.Id) has an exact name match with search string '$secretName'" + $targetSecret = $potentialSecret + break + } + } + + if ($null -eq $targetSecret) { + + Write-Error "$logLead : Could not find an exact match to search string '$secretName' and more than one partial match was found. Returning null" + return $null + } + } else { + + # Only 1 result was found, we're using it + $targetSecret = $matchingSecrets.Records | Select-Object -First 1 + } + } + + $targetSecretId = $targetSecret.Id + Write-Host "$logLead : Pulling secret details for secret with ID '$targetSecretId'" + $secret = $secretServer.GetSecretById($targetSecretId) + + $username = ($secret.Items | Where-Object { $_.fieldName -eq "Username" } | Select-Object -First 1).ItemValue + $password = ($secret.Items | Where-Object { $_.fieldName -eq "Password" } | Select-Object -First 1).ItemValue + + $returnEarly = $false + if([string]::IsNullOrWhiteSpace($username)) { + Write-Error "$loglead : Could not find Username value in secret '$($secret.Name)'" + $returnEarly = $true + } elseif([string]::IsNullOrWhiteSpace($password)) { + Write-Error "$loglead : Could not find Password value in secret '$($secret.Name)'" + $returnEarly = $true + } + + if ($returnEarly) { + + Write-Warning "$logLead : Either the Username or Password (or both) were null/empty for secret '$secretName'" + return $null + } + + return ( New-Object System.Management.Automation.PSCredential ( $username , ( Get-SecureString $password ) ) ) +} diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllDesignationsByEnvironmentType.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllDesignationsByEnvironmentType.ps1 new file mode 100644 index 0000000..429f959 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllDesignationsByEnvironmentType.ps1 @@ -0,0 +1,67 @@ +function Invoke-GetAllDesignationsByEnvironmentType { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeDesignation/MaximizeDesignation functions. + +.DESCRIPTION +Gets all EC2 instances where the alk:env tag value = parameter EnvironmentName. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER EnvironmentName +Environment name to get instances by. Examples are 'dev','staging','qa'. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.PARAMETER ExclusionListCSV +Comma-seperated list of alk:designation strings to exclude from the returned list. Case sensitive. + +.EXAMPLE +Invoke-GetAllDesignationsByEnvironmentType -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -environmentName "dev" -apiGatewayKey "123456789" -exclusionListCSV "Red1,Red17" + +returns JSON: +[ + { + "Designation": "tde2", + "Environment": "dev" + }, + { + "Designation": "FabricSeed", + "Environment": "dev" + }, + { + "Designation": "ai", + "Environment": "dev" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $EnvironmentName, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $false)] + [string] $ExclusionListCSV + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + if(![string]::IsNullOrEmpty($ExclusionListCSV)) { + $ExclusionListCSV = $ExclusionListCSV.Replace(',', '","') + } + + $jsonBody = '{"EnvironmentType":"' + $EnvironmentName + '","ExclusionList":["' + $ExclusionListCSV + '"]}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/GetAllDesignationsByEnvironmentType" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllHostsByDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllHostsByDesignation.ps1 new file mode 100644 index 0000000..52b2b7e --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-GetAllHostsByDesignation.ps1 @@ -0,0 +1,59 @@ +function Invoke-GetAllHostsByDesignation { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeEnvironment/MaximizeEnvironment functions. + +.DESCRIPTION +Gets all EC2 instances where the alk:designation/alk:lane tag value = parameter Designation. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER Designation +Designation name to get instances by. Examples are 'ci1','Red17'. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-GetAllHostsByDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "Red17" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "Red17", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"Designation":"' + $Designation + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/GetAllHostsByDesignation" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByDesignation.ps1 new file mode 100644 index 0000000..b150dd0 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByDesignation.ps1 @@ -0,0 +1,59 @@ +function Invoke-GetCurrentStatusByDesignation { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeDesignation/MaximizeDesignation functions. + +.DESCRIPTION +Gets description of all EC2 instances where the alk:designation tag value = parameter Designation. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER Designation +alk:designation tag to filter which instances to query. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-GetCurrentStatusByDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "ci1" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"Designation":"' + $Designation + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/GetCurrentStatusByDesignation" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByHostnames.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByHostnames.ps1 new file mode 100644 index 0000000..9b93d9c --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-GetCurrentStatusByHostnames.ps1 @@ -0,0 +1,62 @@ +function Invoke-GetCurrentStatusByHostnames { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeDesignation/MaximizeDesignation functions. + +.DESCRIPTION +Gets description of all EC2 instances where the alk:hostname tag value = parameter HostnamesCSV. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER HostnamesCSV +alk:hostname tag to filter which instances to query. Comma-seperated list. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-GetCurrentStatusByHostnames -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -hostnamesCsv "web27425" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $HostnamesCSV + + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $HostnamesCSV = $HostnamesCSV.Replace(',', '","') + + $jsonBody = '{"Hostnames":["' + $HostnamesCSV + '"]}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/GetCurrentStatusByHostnames" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-GetDesignationExclusionsByEnvironmentType.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-GetDesignationExclusionsByEnvironmentType.ps1 new file mode 100644 index 0000000..8769d89 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-GetDesignationExclusionsByEnvironmentType.ps1 @@ -0,0 +1,51 @@ +function Invoke-GetDesignationExclusionsByEnvironmentType { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for determining excluded designations. + +.DESCRIPTION +Gets designations by environment type that will be excluded from scale-down for that day. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER EnvironmentType +Environment name to get designations by. Examples are 'dev','staging','qa'. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-GetDesignationExclusionsByEnvironmentType -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -environmentType "dev" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "Designation": "Ci1", + "Environment": "dev" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $EnvironmentType + + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"EnvironmentType":"' + $EnvironmentType + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/GetDesignationExclusionsByEnvironmentType" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-MaximizeDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-MaximizeDesignation.ps1 new file mode 100644 index 0000000..8622048 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-MaximizeDesignation.ps1 @@ -0,0 +1,64 @@ +function Invoke-MaximizeDesignation { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Scales the environment up. Turns on all EC2 instances by designation. + +.DESCRIPTION +Sends startup commands to EC2 instances where the alk:designation tag value = parameter Designation. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER Designation +alk:designation tag to filter which instances to query. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.PARAMETER IsTest +Switch to determine if it should actually send the commands to the EC2 instances, or just report back what it will do, without actually sending instance commands. + +.EXAMPLE +Invoke-MaximizeDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "ci1" -apiGatewayKey "123456789" -isTest $true + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [switch] $IsTest + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"Designation":"' + $Designation + '","IsTest":"' + $IsTest + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/MaximizeDesignation" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-MinimizeDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-MinimizeDesignation.ps1 new file mode 100644 index 0000000..8c8fbaf --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-MinimizeDesignation.ps1 @@ -0,0 +1,75 @@ +function Invoke-MinimizeDesignation { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Scales the environment down to a 1x1x1. + +.DESCRIPTION +Sends shutdown commands to EC2 instances where the alk:designation tag value = parameter Designation, keeping 1 each of App, Mic and Web running. 1x1x1. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER Designation +alk:designation tag to filter which instances to query. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.PARAMETER IsTest +Switch to determine if it should actually send the commands to the EC2 instances, or just report back what it will do, without actually sending instance commands. + +.PARAMETER ExclusionListCSV +Comma-seperated list of alk:hostname strings to exclude from the shutdown command. Case sensitive. + +.EXAMPLE +Invoke-MinimizeDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "ci1" -apiGatewayKey "123456789" -isTest $true -exclusionList "app2778144" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": false, + "IsStopped": true, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] + +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $IsTest, + [Parameter(Mandatory = $false)] + [AllowEmptyString()] + [string] $ExclusionListCSV + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $ExclusionListCSV = $ExclusionListCSV.Replace(',', '","') + + + $jsonBody = '{"Designation":"' + $Designation + '","ExclusionList":["' + $ExclusionListCSV + '"],"IsTest":"' + $IsTest + '"}' + #Write-Host "Calling Invoke-MinimizeEnvironment with: $jsonBody" + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/MinimizeDesignation" -Headers @{"x-api-key" = $ApiGatewayKey} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-RemoveShutdownPendingTag.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-RemoveShutdownPendingTag.ps1 new file mode 100644 index 0000000..fe5469e --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-RemoveShutdownPendingTag.ps1 @@ -0,0 +1,63 @@ +function Invoke-RemoveShutdownPendingTag { + <# + .SYNOPSIS + This wraps the APIGateway/Lambda for scaling environments. Removes "alk:autoshutdown = pending" tag from hostnames. + + .DESCRIPTION + Removes the tag "alk:autoshutdown = pending" from hosts passed in. + + .PARAMETER Fqdn + Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + + .PARAMETER HostnamesCSV + alk:hostname tag to filter which instances to query. Comma-seperated list. Case sensitive. + + .PARAMETER ApiGatewayKey + Unique key for authenticating to the API Gateway. + + .EXAMPLE + $hosts = @("app2775216","app2776249") + $hostsCsv = ($hosts -join ',') + Invoke-RemoveShutdownPendingTag -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -hostnamesCsv $hostsCsv -apiGatewayKey "123456789" + + returns JSON: + [ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } + ] + .NOTES + Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com + Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com + Qa FQDN : + #> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $HostnamesCSV + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $HostnamesCSV = $HostnamesCSV.Replace(',', '","') + + $jsonBody = '{"Hostnames":["' + $HostnamesCSV + '"]}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/RemoveShutdownPendingTag" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StartDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StartDesignation.ps1 new file mode 100644 index 0000000..cb57087 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StartDesignation.ps1 @@ -0,0 +1,59 @@ +function Invoke-StartDesignation { + <# + .SYNOPSIS + This wraps the APIGateway/Lambda for scaling environments. Starts all instances by Designation. + + .DESCRIPTION + Will start all instances by Designation tag. + + .PARAMETER Fqdn + Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + + .PARAMETER Designation + alk:designation tag to filter which instances to query. Case sensitive. + + .PARAMETER ApiGatewayKey + Unique key for authenticating to the API Gateway. + + .EXAMPLE + Invoke-StartDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "ci1" -apiGatewayKey "123456789" + + returns JSON: + [ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } + ] + .NOTES + Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com + Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com + Qa FQDN : + #> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"Designation":"' + $Designation + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StartDesignation" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response + } \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StartEnvironment.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StartEnvironment.ps1 new file mode 100644 index 0000000..4b39976 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StartEnvironment.ps1 @@ -0,0 +1,59 @@ +function Invoke-StartEnvironment { + <# + .SYNOPSIS + This wraps the APIGateway/Lambda for scaling environments. Starts all instances by environment name, eg. Dev,Qa,Staging. + + .DESCRIPTION + Will start all instances by environment name. + + .PARAMETER Fqdn + Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + + .PARAMETER EnvironmentName + Environment name to get instances by. Examples are 'dev','staging','qa'. Case sensitive. + + .PARAMETER ApiGatewayKey + Unique key for authenticating to the API Gateway. + + .EXAMPLE + Invoke-StartEnvironment -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -environmentName "dev" -apiGatewayKey "123456789" + + returns JSON: + [ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } + ] + .NOTES + Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com + Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com + Qa FQDN : + #> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $EnvironmentName, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"EnvironmentType":"' + $EnvironmentName + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StartEnvironment" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response + } \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StartServers.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StartServers.ps1 new file mode 100644 index 0000000..64e1895 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StartServers.ps1 @@ -0,0 +1,62 @@ +function Invoke-StartServers { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeDesignation/MaximizeDesignation functions. + +.DESCRIPTION +Sends Start command to all EC2 instances where the alk:hostname tag value = parameter HostnamesCSV. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER HostnamesCSV +alk:hostname tag to filter which instances to start. Comma-seperated list. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-StartServers -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -hostnamesCsv "web27425" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $HostnamesCSV + + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $HostnamesCSV = $HostnamesCSV.Replace(',', '","') + + $jsonBody = '{"Hostnames":["' + $HostnamesCSV + '"]}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StartServers" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StopDesignation.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StopDesignation.ps1 new file mode 100644 index 0000000..bd3769d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StopDesignation.ps1 @@ -0,0 +1,59 @@ +function Invoke-StopDesignation { + <# + .SYNOPSIS + This wraps the APIGateway/Lambda for scaling environments. Stops all instances by Designation. + + .DESCRIPTION + Will stop all instances by Designation tag except those that are configured to not be stopped. + + .PARAMETER Fqdn + Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + + .PARAMETER Designation + alk:designation tag to filter which instances to query. Case sensitive. + + .PARAMETER ApiGatewayKey + Unique key for authenticating to the API Gateway. + + .EXAMPLE + Invoke-StopDesignation -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -designation "ci1" -apiGatewayKey "123456789" + + returns JSON: + [ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } + ] + .NOTES + Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com + Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com + Qa FQDN : + #> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $Designation, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"Designation":"' + $Designation + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StopDesignation" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response + } \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StopEnvironment.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StopEnvironment.ps1 new file mode 100644 index 0000000..4cfc220 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StopEnvironment.ps1 @@ -0,0 +1,59 @@ +function Invoke-StopEnvironment { + <# + .SYNOPSIS + This wraps the APIGateway/Lambda for scaling environments. Stops all instances by environment name, eg. Dev,Qa,Staging. + + .DESCRIPTION + Will stop all instances by environment name except those that are configured to not be stopped. + + .PARAMETER Fqdn + Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + + .PARAMETER EnvironmentName + Environment name to get instances by. Examples are 'dev','staging','qa'. Case sensitive. + + .PARAMETER ApiGatewayKey + Unique key for authenticating to the API Gateway. + + .EXAMPLE + Invoke-StopEnvironment -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -environmentName "dev" -apiGatewayKey "123456789" + + returns JSON: + [ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": true, + "IsStopped": false, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } + ] + .NOTES + Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com + Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com + Qa FQDN : + #> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $EnvironmentName, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $jsonBody = '{"EnvironmentType":"' + $EnvironmentName + '"}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StopEnvironment" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response + } \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Invoke-StopServers.ps1 b/Modules/Alkami.DevOps.Common/Public/Invoke-StopServers.ps1 new file mode 100644 index 0000000..4f3b8be --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Invoke-StopServers.ps1 @@ -0,0 +1,62 @@ +function Invoke-StopServers { +<# +.SYNOPSIS +This wraps the APIGateway/Lambda for scaling environments. Helper for the MinimizeDesignation/MaximizeDesignation functions. + +.DESCRIPTION +Sends Shutdown command to all EC2 instances where the alk:hostname tag value = parameter HostnamesCSV. + +.PARAMETER Fqdn +Fully Qualified Domain Name of the API Gateway. Each environment/account has a unique API Gateway endpoint. + +.PARAMETER HostnamesCSV +alk:hostname tag to filter which instances to stop. Comma-seperated list. Case sensitive. + +.PARAMETER ApiGatewayKey +Unique key for authenticating to the API Gateway. + +.EXAMPLE +Invoke-StartServers -fqdn "vyq7hqcx55.execute-api.us-east-1.amazonaws.com" -hostnamesCsv "web27425" -apiGatewayKey "123456789" + +returns JSON: +[ + { + "IsExcluded": false, + "IsApp": false, + "IsMic": false, + "IsWeb": true, + "IsRunning": false, + "IsStopped": true, + "InstanceId": "i-0fa21749b4e4b81ea", + "Designation": "ci1", + "HostName": "web27425", + "Environment": "dev", + "Service": "orb" + } +] +.NOTES +Dev FQDN : vyq7hqcx55.execute-api.us-east-1.amazonaws.com +Staging FQDN : cox3133b67.execute-api.us-east-1.amazonaws.com +Qa FQDN : +#> + [CmdletBinding()] + + Param ( + [Parameter(Mandatory = $true)] + [string] $Fqdn, + [Parameter(Mandatory = $true)] + [string] $ApiGatewayKey, + [Parameter(Mandatory = $true)] + [string] $HostnamesCSV + + ) + + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + $HostnamesCSV = $HostnamesCSV.Replace(',', '","') + + $jsonBody = '{"Hostnames":["' + $HostnamesCSV + '"]}' + $response = Invoke-RestMethod -Uri "https://$Fqdn/Prod/StopServers" -Headers @{"x-api-key"="$ApiGatewayKey"} -Method POST -body $jsonBody -ContentType "application/json" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/New-AlkamiServiceFabricClusterByTag.ps1 b/Modules/Alkami.DevOps.Common/Public/New-AlkamiServiceFabricClusterByTag.ps1 new file mode 100644 index 0000000..2769c34 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/New-AlkamiServiceFabricClusterByTag.ps1 @@ -0,0 +1,77 @@ +function New-AlkamiServiceFabricClusterByTag { + +<# +.SYNOPSIS + Creates a service fabric cluster with by pod tag. + Security group prefix is 'Stage' for Staging, 'DEV' for QA. Production is generally pod[X] + +.PARAMETER pod + The pod tag designation for the cluster. +.PARAMETER securityGroupPrefix + The security group prefix for the GMSA accounts. 'Stage' for Staging, 'DEV' for Dev/QA + Production usually follows the convention 'pod[X]' +.PARAMETER adminUsers + The AD groups/users who are allowed access to the service fabric admin functionality on the CLI and Dashboard. +.PARAMETER minBootstrapPhase + The minimum bootstrap phase of servers that are allowed to join the cluster. 95 (at time of writing) is the ServiceFabric bootstrap phase. +.PARAMETER timeout + The amount of time (in minutes) this command will poll for valid servers to join into the cluster. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$pod, + [Parameter(Mandatory = $true)] + [string]$securityGroupPrefix, + [Parameter(Mandatory = $true)] + [string[]]$adminUsers, + [Parameter(Mandatory = $false)] + [string[]]$clientUsers, + [Parameter(Mandatory = $false)] + [string]$role = "app:fab", + [Parameter(Mandatory = $false)] + [int]$minServers = 5, + [Parameter(Mandatory = $false)] + [int]$minBootstrapPhase = 95, + [Parameter(Mandatory = $false)] + [int]$timeout = 15, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + + ) + + $loglead = (Get-LogLeadName) + + Import-AWSModule # SSM + + # Grab the fab server hostnames for the pod. + $serverNames = Get-AlkamiServiceFabricHostnamesByTag -designation $pod -role $role -minServers $minServers -minBootstrapPhase $minBootstrapPhase -timeout $timeout; + + # Create a Service Fabric Cluster. + $splatParams = @{} + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $splatParams["ProfileName"] = "$ProfileName" + } + + if (!([string]::IsNullOrEmpty($Region))) { + $splatParams["Region"] = "$Region" + } + $user = (Get-SSMParameter -Name "account-cicd-username" -WithDecryption $true @splatParams).Value; + $password = ((Get-SSMParameter -Name "account-cicd-password" -WithDecryption $true @splatParams).Value | ConvertTo-SecureString -AsPlainText -Force); + $credential = New-Object System.Management.Automation.PSCredential ($user, $password); + + try { + Enter-PSSession -Credential $credential -ComputerName "." + + New-AlkamiServiceFabricCluster -servers $serverNames -securityGroupPrefix $securityGroupPrefix -adminUsers $adminUsers -clientUsers $clientUsers; + Write-Host "$loglead : Cluster creation complete." + } finally { + Exit-PSSession; + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/New-WebTierMachineConfigAppSettings.ps1 b/Modules/Alkami.DevOps.Common/Public/New-WebTierMachineConfigAppSettings.ps1 new file mode 100644 index 0000000..c9603b3 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/New-WebTierMachineConfigAppSettings.ps1 @@ -0,0 +1,81 @@ +function New-WebTierMachineConfigAppSettings { +<# +.SYNOPSIS + Upserts Web Tier Machine Config App Settings. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + [XML]$machineConfig = Read-MachineConfig + [System.Xml.XmlElement]$configRoot = $machineConfig.configuration + $machineConfigIsDirty = $false + + $appSettingsNode = $configRoot.SelectSingleNode("//appSettings") + + if ($null -eq $appSettingsNode) { + Write-Verbose "$logLead : Creating appSettings Node" + $appSettingsNode = $machineConfig.CreateElement("appSettings") + $configRoot.AppendChild($appSettingsNode) | Out-Null + } + + + # Add App Settings + foreach ($setting in $webTierAppSettings) { + # Some settings are derived values which depend on other settings being instrumented. + # We will change the value to REPLACEME here if the dependent values are not properly set so that they're skipped + if ($setting.Name -eq "ReportServerUrl" -and ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServer"}).Value -eq "REPLACEME") { + $setting.Value = "REPLACEME" + } + elseif ($setting.Name -eq "ReportServerPath" -and $setting.Value -eq "/" -and [String]::IsNullOrEmpty([Environment]::GetEnvironmentVariable("POD", "Machine"))) { + $setting.Value = "REPLACEME" + } + + $appSetting = $appSettingsNode.SelectSingleNode(("//add[@key='{0}']" -f $setting.Name)) + + if ($null -eq $appSetting) { + Write-Output ("$logLead : Adding {0} node with value {1}" -f $setting.Name, $setting.Value) + $appSettingElement = $machineConfig.CreateElement("add") + $appSettingElement.SetAttribute("key", $setting.Name) + + if ($setting.Value -eq "REPLACEME") { + Write-Warning ("$logLead : Value read as REPLACEME. Node {0} will be added with an empty value" -f $setting.Name) + $appSettingElement.SetAttribute("value", "") + } + else { + $appSettingElement.SetAttribute("value", $setting.Value) + } + + $appSettingsNode.AppendChild($appSettingElement) | Out-Null + $machineConfigIsDirty = $true + } + elseif ($appSetting.Attributes["value"].Value -ne $setting.Value) { + if ($setting.Value -ne "REPLACEME" -and $setting.Name -ne "ReportServerPath") { + Write-Output ("$logLead : Update setting {0} value from {1} to {2}" -f $setting.Name, $appSetting.Attributes["value"].Value, $setting.Value) + $appSetting.SetAttribute("value", $setting.Value) + $machineConfigIsDirty = $true + } + elseif ($setting.Value -ne "REPLACEME" -and $setting.Name -eq "ReportServerPath") { + Write-Warning ("$logLead : The ReportServerPath setting exists but does not follow convention. Please confirm the value is correct. Value in machine.config: {0}. Value in script: {1}" -f $appSetting.Attributes["value"].Value, $setting.Value) + } + else { + Write-Warning ("$logLead : Value read as REPLACEME. Node {0} will not be updated" -f $setting.Name) + } + } + else { + Write-Verbose ("$logLead : AppSetting {0} already exists with correct value" -f $setting.Name) + } + } + + if ($machineConfigIsDirty) { + Write-Output ("$logLead : Saving Modified machine.config") + $machineConfig.Save($machineConfigPath) + } + else { + Write-Output ("$logLead : No changes required to the machine.config") + } +} + +Set-Alias -name Create-WebTierMachineConfigAppSettings -value New-WebTierMachineConfigAppSettings; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.ps1 b/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.ps1 new file mode 100644 index 0000000..e5a5e67 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.ps1 @@ -0,0 +1,163 @@ +function Publish-MessageToSlack { +<# +.SYNOPSIS + Posts a message to Slack. + +.DESCRIPTION + Posts a message to Slack. + +.PARAMETER Channels + Array of channels to post to, such as @("@username","#some-channel-name") + +.PARAMETER SlackHookUrl + Slack webhook url + +.PARAMETER SlackParameters + Hashtable of "messagebody" parameters. Cannot be used in conjunction with + MessageText, UserName and IconEmoji. This is used when the caller wants to + build their own message body JSON. + +.PARAMETER SlackAttachments + Hashtable of Slack message attachments to be added to the "messagebody" + +.PARAMETER MessageText + String of simple message text. If this is populated, MessageBody must be + left empty or MessageBody will be used instead. This is used in combination + with UserName and IconEmoji to construct a simple message JSON object. Caller + does not have to build JSON object to send a simple message. + +.EXAMPLE + $chan = @("@trowton","#eng-sre-ci-cd-alerts") + $buildlog = "http://duckduckgo.com" + $fallback = "View the Build: $buildLog" + $actions = @( + @{ + "type" = "button" + "text" = "Build Log" + "url" = $buildLog + } + ) + $attachments = @{ + fallback = $fallback + color = "#009EFF" + actions = $actions + } + $params = @{ + "username"="someuser";"icon_emoji"=":teamcity:";"text"="Text [with square brackets] and a period." + } + + Publish-MessageToSlack -Channels $chan -SlackHookUrl $slackHookUrl -MessageBody $params -SlackAttachments $attachments + + Publish-MessageToSlack -Channels $chan -SlackHookUrl $slackHookUrl -MessageText "sampletext" -IconEmoji ":smiley:" -Username "TC job" -SlackAttachments $attachments + +.LINK + https://api.slack.com/methods/chat.postMessage + +.NOTES + Either MessageBody or MessageText must be populated to send a message. If MessageBody + is populated, it will take precedence over MessageText. +#> + [CmdletBinding()] + [OutputType([void])] + param( + [Parameter(Mandatory = $True)] + [array]$Channels, + [Parameter(Mandatory = $True)] + [string]$SlackHookUrl, + [Parameter(Mandatory = $False)] + [Alias ("MessageBody")] + [hashtable]$SlackParameters, + [Parameter(Mandatory = $False)] + [hashtable]$SlackAttachments, + [Parameter(Mandatory = $False)] + [string] $MessageText, + [Parameter(Mandatory = $False)] + [string] $Username = "Publish Message", + [Parameter(Mandatory = $False)] + [string] $IconEmoji = ":slack:" + ) + + $loglead = (Get-LogLeadName) + + # Test if either MessageBody and MessageText are populated. One must be populated. + if($null -eq $SlackParameters -and [string]::IsNullOrWhiteSpace($MessageText)) { + throw "SlackParameters or MessageText must be populated to send a message!" + } + + # Test if both MessageBody and MessageText are populated. + if($null -ne $SlackParameters -and !([string]::IsNullOrWhiteSpace($MessageText))) { + Write-Host "$loglead Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." + } + + # If MessageText is populated and SlackParameters is null, build a body. + if($null -eq $SlackParameters) { + Write-Verbose "$loglead SlackParameters is null, using MessageText [$MessageText], UserName [$UserName] and Icon [$IconEmoji] to send message." + $messageBody = @{ + "username" = "$Username"; + "text" = "$MessageText"; + "icon_emoji" = "$IconEmoji"; + } + } else { + # Parameters are a direct mapping to the message body. + $messageBody = $SlackParameters + } + + # Add attachments to message body, if they're provided. + if ($null -ne $SlackAttachments) { + $messageBody["attachments"] = @($SlackAttachments) + } + + # Test for required keys in attachments. + if($messageBody["attachments"].Count -gt 0) { + $hasFallback = $false + foreach ($attachment in $messageBody["attachments"]) { + foreach ($aKey in $attachment.Keys) { + if ($aKey -eq "fallback") { + $hasFallback = $true + } + } + } + + if ($hasFallback -eq $false) { + throw "Missing required parameter key 'fallback' on 'SlackAttachments'." + } + } + + # Test for 'text' key in MessageBody. + $hasText = $false + foreach ($param in $messageBody) { + foreach ($key in $param.Keys) { + if ($key -eq "text") { + $hasText = $true + } + } + } + + if ($hasText -eq $false) { + throw "Missing required parameter key 'text' on 'SlackParameters'." + } + + # Check $channels for commas to split on. + if ($Channels[0].Contains(",")) { + Write-Host "$loglead : Channels was supplied as a comma delimited string, not an array. Splitting..." + $sanitizedChannels = $Channels[0].Split(",") + } else { + $sanitizedChannels = $Channels + } + + # Send message to each specified channel(s). + foreach ($channel in $sanitizedChannels) { + $messageBody["channel"] = $channel; + $messageJson = ($messageBody | ConvertTo-Json -Depth 100).Replace('\\r\\n', '\r\n') + Write-Verbose "$loglead : Posting JSON:" + Write-Verbose $messageJson + + try { + (Invoke-RestMethod -Uri $SlackHookUrl -Method POST -Body $messageJson -ContentType "application/json") | Out-Null + } catch { + $errorMessage = $_.Exception.Message + Write-Warning "$logLead : Could not post message to Slack.`nError message is: $errorMessage" + # TODO: Metric this so we know that errors occurred. + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.tests.ps1 b/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.tests.ps1 new file mode 100644 index 0000000..3b87e73 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Publish-MessageToSlack.tests.ps1 @@ -0,0 +1,260 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Publish-MessageToSlack" { + Context "When Sending Messages To Multiple Channels " { + It "Calls Invoke-RestMethod multiple times" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + + Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params + + Assert-MockCalled Invoke-RestMethod -Times 2 -ModuleName $moduleForMock + } + } + + Context "When Message Body Does Not Contain Required Values" { + It "Should Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + # No text value, which is required + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + } + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -slackParameters $params } | Should -Throw "Missing required parameter key 'text' on 'SlackParameters'." + } + } + + Context "When Message Body And Message Text Are Both Empty" { + It "Should Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" } | Should -Throw "SlackParameters or MessageText must be populated to send a message!" + } + } + + Context "When Attachment List Is Invalid" { + It "Should Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + # Actions.url must be in a valid http format. Can't just add "sometext". + $actions = @( + @{ + "type" = "button"; + "text" = "Nginx Server"; + "url" = "http://google.com" + } + ) + + # Attachment is missing fallback, which is required. + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "I am a test with an invalid attachment" + "attachments" = @( + @{ + color = "green"; + actions = $actions + } + ) + } + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -slackParameters $params } | Should -Throw "Missing required parameter key 'fallback' on 'SlackAttachments'." + } + } +} + +Context "When Sending Message is Successful" { + It "Should Not Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + # Actions.url must be in a valid http format. Can't just add "sometext". + $buildLog = "https://google.com" + $fallback = "View the Build: $buildLog" + $actions = @( + @{ + "type" = "button" + "text" = "Build Log" + "url" = $buildLog + } + ) + $attachments = @{ + fallback = $fallback + color = "#009EFF" + actions = $actions + } + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params -SlackAttachments $attachments } | Should -Not -Throw + + Assert-MockCalled Write-Host -Exactly 0 -ModuleName $moduleForMock -Scope It + } + + It "Should Not Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageText "sample text" -IconEmoji ":slack:" -Username "someuser" } | Should -Not -Throw + + Assert-MockCalled Write-Host -Exactly 0 -ModuleName $moduleForMock -Scope It + } + + It "Should Not Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params -MessageText "sample text" -IconEmoji ":slack:" -Username "someuser" } | Should -Not -Throw + + Assert-MockCalled Write-Host -Exactly 1 -ModuleName $moduleForMock -Scope It + } + + It "Should Call Invoke-RestMethod" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + + Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params + + Assert-MockCalled Invoke-RestMethod -ModuleName $moduleForMock + } +} + +Context "When Setting Both MessageBody And MessageText" { + It "Should Not Throw" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + + $messageText = "sampletext" + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + { Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params -MessageText $messageText } | Should -Not -Throw + } + + It "Should Write Override Output" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + + $messageText = "sampletext" + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params -MessageText $messageText + + Assert-MockCalled Write-Host -Exactly 1 -ModuleName $moduleForMock -Scope It + } + + It "Should Call Invoke-RestMethod" { + $channels = @( + "#testChannel1", + "#testChannel2" + ) + + $params = @{ + "username" = "@user1" + "icon_emoji" = ":smiley:" + "text" = "sampletext" + } + + $messageText = "sampletext" + + Mock -ModuleName $moduleForMock Invoke-RestMethod { return $true } + Mock Write-Host -ModuleName $moduleForMock ` + -ParameterFilter { $Object -like "*Both SlackParameters and MessageText parameters are not null. Choosing SlackParameters as message body." } ` + -MockWith { } + + Publish-MessageToSlack -channels $channels -slackHookUrl "www.dummyurl.com" -MessageBody $params -MessageText $messageText + + Assert-MockCalled Invoke-RestMethod -ModuleName $moduleForMock + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Reset-ASInstanceHealth.ps1 b/Modules/Alkami.DevOps.Common/Public/Reset-ASInstanceHealth.ps1 new file mode 100644 index 0000000..0e65b4d --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Reset-ASInstanceHealth.ps1 @@ -0,0 +1,89 @@ +function Reset-ASInstanceHealth { +<# +.SYNOPSIS + Method to reset instance health status as reported to its auto scale group. + +.DESCRIPTION + Will attempt to set instance health to "Healthy" for all servers passed in. Will sleep for the defined wait period (2 minutes default) + and query again the status. If any are still reporting as "Unhealthy" then this will Write-Error. + +.PARAMETER Servers + List of servers to work with. Usually grouped by App, Web. Assumes all servers passed in are in the same ASG. Assumes all servers have + "".fh.local" appended to the hostname. + +.PARAMETER SkipHealthCheck + Switch to skip the 2 minute wait period and return immediately following the set operation. + +.PARAMETER WaitSeconds + Number of seconds to wait. Default if not provided is 120 seconds (2 minutes.) +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Alias('Computers')] + [string[]]$Servers, + + [Parameter(Mandatory = $true)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [switch]$SkipHealthCheck = $false, + + [Parameter(Mandatory = $false)] + [Alias('Sleep')] + [int]$WaitSeconds = 120 + ) + + $allHealthy = $true + $logLead = (Get-LogLeadName) + $instanceHash = @{} + + Import-AWSModule # AS + + # Test each server for "Healthy" ASG status and set to "Healthy" if it's not. + foreach ($computerName in $servers) { + Write-Verbose "$logLead : Attempting to Get Computer $computerName ASG status." + # $getHealthStatusScriptBlock + $region = Get-AwsRegionByHostname -ComputerName $computerName + $currentInstance = Get-EC2InstancesByHostname -Servers $computerName -ProfileName $ProfileName + $instanceHash += @{$computerName = $currentInstance } + $asInstance = Get-ASAutoScalingInstance -InstanceId $currentInstance.InstanceId -Region $region -ProfileName $ProfileName + Write-Verbose "$logLead : $computerName status is $($asInstance.HealthStatus)" + + if ($asInstance.HealthStatus -ne "HEALTHY") { + Write-Verbose "$logLead : $computerName is not well. Setting to Healthy status." + Set-ASInstanceHealth -InstanceId $currentInstance.InstanceId -HealthStatus "Healthy" -ProfileName $ProfileName -Region $region -Force + $allHealthy = $false + } + } + + # Take a nap if not all healthy. This allows the ASG to re-report any that switch back to "Unhealthy". + if (!$allHealthy -and !$SkipHealthCheck) { + Write-Host "$logLead : Not all instances are reporting Healthy. Setting to Healthy status and sleeping for $waitSeconds seconds." + Start-Sleep -Seconds $WaitSeconds + Write-Host "$logLead : Checking again for health status." + + # Array to hold any errors. + $errors = @() + + # Check again for "Healthy" ASG status on all servers. + foreach ($computerName in $servers) { + Write-Verbose "$logLead : Attempting to Get Computer $computerName ASG status." + $currentInstance = $instanceHash.$computerName + $actionResult = Get-ASAutoScalingInstance -InstanceId $currentInstance.InstanceId -Region $region -ProfileName $ProfileName + Write-Verbose "$logLead : $computerName status is $($actionResult.HealthStatus)" + + if ($actionResult.HealthStatus -ne "HEALTHY") { + # All-stop when one switches back to "Unhealthy". + $errors += "$logLead : Computer $computerName is not in a Healthy status as reported to the ASG." + } + Write-Verbose "$logLead : $computerName status is $actionResult" + } + + if (!(Test-IsCollectionNullOrEmpty $errors)) { + Write-Warning "Error(s) found" + $allErrors = $errors -Join "," + throw $allErrors + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Select-AvailableWinRmHosts.ps1 b/Modules/Alkami.DevOps.Common/Public/Select-AvailableWinRmHosts.ps1 new file mode 100644 index 0000000..b579270 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Select-AvailableWinRmHosts.ps1 @@ -0,0 +1,46 @@ +function Select-AvailableWinRmHosts { + <# + .SYNOPSIS + Returns a filtered list of servers that are responding to WinRM requests. + + .PARAMETER ComputerName + List of servers to test WinRM connections against. + + .PARAMETER ReturnBadServers + Returns the list of servers that could not be connected to. + #> + [CmdletBinding()] + param( + [string[]]$ComputerName, + [switch]$ReturnBadServers + ) + + if(Test-IsCollectionNullOrEmpty $ComputerName) { + return $null + } + + $results = Invoke-Parallel -objects $ComputerName -returnObjects -numThreads 32 -script { + param($server) + + # Invoke a simple script block to verify that WinRM is listening. + try { + $success = (1 -eq (Invoke-Command -ComputerName $server -ErrorAction Stop -ScriptBlock { return 1 })) + if($success) { + return $server + } else { + return $null + } + } catch { + Write-Warning "Select-AvailableWinRMHosts: Could not connect to [$server]`n$_" + return $null + } + } + $results = ($results | Where-Object { $null -ne $_ }) + + # Produce a list of the unavailable servers from the available servers. + if($ReturnBadServers.IsPresent) { + $results = $ComputerName | Where-Object { $results -notcontains $_ } + } + + return $results +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/Public/Send-ObjectAsHtmlTable.ps1 b/Modules/Alkami.DevOps.Common/Public/Send-ObjectAsHtmlTable.ps1 new file mode 100644 index 0000000..bcc891b --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Send-ObjectAsHtmlTable.ps1 @@ -0,0 +1,73 @@ +function Send-ObjectAsHtmlTable { +<# +.SYNOPSIS +Sends a formatted email representing a given powershell object. + +.DESCRIPTION +Translates a powershell object into an html table, keeping the ordering of the +properties (if they were ordered). Joins the object html to an html template +located in the modules file list and emails it to the specified recipients. + +.PARAMETER ToAddress +[string[]]One or more email address to send the powershell object to. + +.PARAMETER FromAddress +[string]The from address for this email, defaults to 'SreAlerts@alkamitech.com' + +.PARAMETER Subject +[string] The subject of the email you wish to send + +.PARAMETER SMTPUsername +[string] The username which has access to the mail server. + +.PARAMETER SMTPPassword +[string] The password for the user that has access to the mail server. + +.PARAMETER SMTPServer +[string] The url of the SMTPServer + +.EXAMPLE +Send-ObjectAsHtmlTable -ToAddress "toUser@alkamitech.com" -Subject "Something Happened" -InputObject (get-process | Select-Object -First 1 Handles,ProcessName) ` + -SMTPUsername "user@smtp.com -SMTPPassword "anSmtpPassword" -SMTPServer "smtp.someServer.org" + +Will translate the input object and converting it's properties, in this case 'Handles' and 'ProcessName' into fields for an html table. +Then join the html table to an email template and send to the user specified in the to address. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True)] + [string[]]$ToAddress, + [Parameter()] + [string]$FromAddress = "SreAlerts@alkamitech.com", + [Parameter(Mandatory=$True)] + [string]$Subject, + [Parameter(Mandatory=$True)] + [object[]]$InputObject, + [Parameter(Mandatory=$True)] + [string]$SMTPUsername, + [Parameter(Mandatory=$True)] + [SecureString]$SMTPPassword, + [Parameter(Mandatory=$True)] + [string]$SMTPServer + ) + begin{ + $Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $SMTPUsername, $SMTPPassword + if(!$Credential){throw "Credentials for sending mail message could not be created"} + + $HtmlTemplatePath = $MyInvocation.MyCommand.Module.FileList | Where-Object {$_ -match "ObjectAsTableTemplate.html"} + if(!(Test-Path $HtmlTemplatePath)){throw "HTML Template could not be found at path $HtmlTemplatePath"} + $HtmlTemplate = Get-Content $HtmlTemplatePath | Out-String + } + process{ + + $headers = $InputObject | Select-Object -first 1 | ForEach-Object {$_.psobject.Properties | ForEach-Object {$_.Name}} + $width = "width:$(100/ $headers.Count)%" + $headerData = $headers | ForEach-Object {"$_"} + $bodyData = $InputObject | ForEach-Object {"`n";$obj = $_;$headers | ForEach-Object {"`n$($obj.$_)"};"`n"} + + $HtmlTemplate = $HtmlTemplate.Replace("[TableHeaderData]",$headerData) + $HtmlTemplate = $HtmlTemplate.Replace("[TableBodyData]",$bodyData) + + Send-MailMessage -Body $HtmlTemplate -BodyAsHtml -from $FromAddress -To $ToAddress -Subject $Subject -UseSsl -Credential $Credential -Port 587 -SmtpServer $SMTPServer + } +} diff --git a/Modules/Alkami.DevOps.Common/Public/Test-IsTeamCityProcess.ps1 b/Modules/Alkami.DevOps.Common/Public/Test-IsTeamCityProcess.ps1 new file mode 100644 index 0000000..fa9d1e2 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Public/Test-IsTeamCityProcess.ps1 @@ -0,0 +1,25 @@ +function Test-IsTeamCityProcess { +<# +.SYNOPSIS + Determines if the Current Process is a TeamCity Agent Process. + +.NOTES + Will not work except when running in a direct agent process. Checks for TeamCity agent environment + variables to exist. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + $teamCityJREVariable = $ENV:TEAMCITY_JRE + + if ([String]::IsNullOrEmpty($teamCityJREVariable)) { + + Write-Verbose "$logLead : TEAMCITY_JRE User Environment Variable Not Found" + return $false + } + + return $true +} diff --git a/Modules/Alkami.DevOps.Common/Resources/Formatters/ConvertTo-AwsCredentialEntry.ps1xml b/Modules/Alkami.DevOps.Common/Resources/Formatters/ConvertTo-AwsCredentialEntry.ps1xml new file mode 100644 index 0000000..27e3713 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Resources/Formatters/ConvertTo-AwsCredentialEntry.ps1xml @@ -0,0 +1,109 @@ + + + + + + AlkamiFormatters + + + + AwsCredentialEntry + + + + + + + + DefaultAWSCredentialEntryView + + + + AWSTypes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + + + role_arn + + + + + mfa_serial + + + + + region + + + + + source_profile + + + + + credential_process + + + + + output + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Common/Resources/ObjectAsTableTemplate.html b/Modules/Alkami.DevOps.Common/Resources/ObjectAsTableTemplate.html new file mode 100644 index 0000000..0af3128 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/Resources/ObjectAsTableTemplate.html @@ -0,0 +1,44 @@ + + + + + +
+ Alkami Technology +
+ + + [TableHeaderData] + + [TableBodyData] +
+
+ + diff --git a/Modules/Alkami.DevOps.Common/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Common/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..d36dca2 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/tools/chocolateyInstall.ps1 @@ -0,0 +1,67 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + ###### + ## Begin Very Special Logic + ###### + + ## Kill Alkami.DevOps.Deployment if it exists. This is a legacy concern + + $pathToKill = "c:\Program Files\WindowsPowerShell\Modules\Alkami.DevOps.Deployment"; + if (Test-Path $pathToKill) { + Remove-Item $pathToKill -Recurse -Force; + } + + $chocoInstallPath = Get-ChocolateyInstallPath + $pathToKill = Join-Path $chocoInstallPath "lib\Alkami.DevOps.Deployment" + if (Test-Path $pathToKill) { + Remove-Item $pathToKill -Recurse -Force; + } + + ###### + ## End Very Special Logic + ###### + + + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + $resourcesFolder = (Join-Path $parentPath "Resources") + if (Test-Path $resourcesFolder) { + + Write-Host "Copying resources folder for module $id to [$targetModuleVersionPath]" + Copy-Item $resourcesFolder -Destination $targetModuleVersionPath -Recurse -Force + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Common/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Common/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.Common/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.nuspec b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.nuspec new file mode 100644 index 0000000..26ec659 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.nuspec @@ -0,0 +1,39 @@ + + + + Alkami.DevOps.Installation + $version$ + Alkami Platform Modules - DevOps - Installation + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Installation + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps Installation module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.psd1 b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.psd1 new file mode 100644 index 0000000..08d8ed4 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.psd1 @@ -0,0 +1,21 @@ +@{ + RootModule = 'Alkami.DevOps.Installation.psm1' + ModuleVersion = '3.25.0' + GUID = 'dc9924e9-7646-4e11-ba59-48d6a715617c' + Author = 'SRE,dsage,cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A set of functions to configure servers and install the ORB application' + RequiredModules = 'Alkami.PowerShell.Common', 'Alkami.PowerShell.IIS', 'Alkami.DevOps.Common', 'Alkami.DevOps.Certificates', 'Alkami.PowerShell.Configuration', 'Alkami.PowerShell.AD', 'Alkami.PowerShell.Services', 'Alkami.Ops.SecretServer', 'Alkami.Ops.Common' + FunctionsToExport = 'Add-OldHotfixPackagesToUninstallList','Add-OverflowCustomAttribute','Copy-NewRelicCustomInstrumentationFiles','Get-BadPackages','Get-ClientWebSiteInformationFromDatabase','Get-EnvironmentData','Get-MicroserviceNewRelicMapping','Get-NewRelicAccountDetails','Get-NewRelicYamlPath','Get-PackageServerMapping','Get-ServerPackageInformation','Install-NewRelicDotNetAgent','Install-NewRelicInfrastructure','Install-NewRelicServerMonitor','Install-ORB','Install-ORBAppServer','Install-ORBWebServer','New-DummyPackageInstallationData','New-PackageMetadataObject','New-WebTierWebSites','Read-AppTierSecrets','Read-WebTierSecrets','Remove-OutdatedNewHotfixPackages','Set-InfrastructureConfiguration','Set-NewRelicConfigurationValues','Set-NewRelicDeployment','Set-RapidFailSettings','Set-ServiceAccountValue','Submit-DeploymentToNewRelic','Uninstall-NewRelicDotNetAgent','Update-Borg','Write-PackageList' + AliasesToExport = 'Create-WebTierWebSites','Load-AppTierSecrets','Load-WebTierSecrets' + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Installation+Module' + FileList = @('.\NewRelicCustomInstrumentation\CustomInstrumentation.xml', '.\NewRelicCustomInstrumentation\RemoveActivity.xml') + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'installation') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Installation+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } +} diff --git a/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.pssproj b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.pssproj new file mode 100644 index 0000000..3515589 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Alkami.DevOps.Installation.pssproj @@ -0,0 +1,90 @@ + + + + Debug + 2.0 + {3e38f1af-8451-4617-a24c-941552efc43a} + Exe + MyApplication + MyApplication + Alkami.DevOps.Installation + + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Installation") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + Alkami.Ops.Common + {fa9745dd-68ac-4194-9c33-acf19411d357} + True + + + Alkami.DevOps.Common + {1bd8fc22-5882-4d5c-8128-81f1d61f8d77} + True + + + Alkami.DevOps.Operations + {6cafc0c6-a428-4d30-a9f9-700e829fea51} + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/AlkamiManifest.xml b/Modules/Alkami.DevOps.Installation/AlkamiManifest.xml new file mode 100644 index 0000000..d995be3 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Installation + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/CustomInstrumentation.xml b/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/CustomInstrumentation.xml new file mode 100644 index 0000000..832e456 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/CustomInstrumentation.xml @@ -0,0 +1,711 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/RemoveActivity.xml b/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/RemoveActivity.xml new file mode 100644 index 0000000..623cb0e --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/NewRelicCustomInstrumentation/RemoveActivity.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Private/Set-AppTierGMSAAccounts.ps1 b/Modules/Alkami.DevOps.Installation/Private/Set-AppTierGMSAAccounts.ps1 new file mode 100644 index 0000000..e91cad4 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Private/Set-AppTierGMSAAccounts.ps1 @@ -0,0 +1,58 @@ +function Set-AppTierGMSAAccounts { +<# +.SYNOPSIS + Sets App Tier GMS Accounts. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("PodGMSAAccount")] + [string]$podGMSAAccountParent + ) + + $DEFAULTVALUE = "DEFAULTVALUE" + + Write-Host "If a warning happens next for [The AppSetting with Key Environment.UserPrefix could not be found], you can ignore that completely" + ## We want to set the value if it does not exist, because eventually all machines should have this configured. + ## This pairs with Alkami.PowerShell.Configuration\Get-AppServiceAccountName to go in the local web.config or app.config + ## Only write it as we consume it. + if ($null -eq (Get-AppSetting -appSettingKey "Environment.UserPrefix")) { + Write-Verbose "Adding Environment.UserPrefix to match this function" + Set-AppSetting -key "Environment.UserPrefix" -Value $podGMSAAccountParent + ## This is so we can use this later as ($domain)\(Get-AppSetting -appSettingKey "Environment.UserPrefix").$MatrixLookup[appName]$ + ## see also Get-AppServiceAccountName + } + + $applicationsDictionary = @{ + 'AuditService' = 'fh\DEFAULTVALUE.audit$'; + 'BankService' = 'fh\DEFAULTVALUE.bank$'; + 'ContentService' = 'fh\DEFAULTVALUE.content$'; + 'CoreService' = 'fh\DEFAULTVALUE.core$'; + 'ExceptionService' = 'fh\DEFAULTVALUE.exception$'; + 'MessageCenterService' = 'fh\DEFAULTVALUE.msgctr$'; + 'NagConfigurationService' = 'fh\DEFAULTVALUE.nag$'; + 'NotificationService' = 'fh\DEFAULTVALUE.notify$'; + 'RP-STS' = 'fh\DEFAULTVALUE.rpsts$'; + 'SchedulerService' = 'fh\DEFAULTVALUE.schedule$'; + 'SecurityManagementService' = 'fh\DEFAULTVALUE.secmgr$'; + 'STSConfiguration' = 'fh\DEFAULTVALUE.stsconf$'; + 'SymConnectMultiplexer' = 'fh\DEFAULTVALUE.multiplx$'; + 'Alkami Radium Scheduler Service' = 'fh\DEFAULTVALUE.radium$'; + 'Alkami Nag Service' = 'fh\DEFAULTVALUE.nag$'; + } + + foreach ($appTierApplication in $appTierApplications) { + $appName = $appTierApplication.Name + $newUserName = $applicationsDictionary[$appName] -replace $DEFAULTVALUE,$podGMSAAccountParent + $appTierApplication.User = $newUserName + Write-Host "$($appTierApplication.Name) : $($appTierApplication.User)" + } + + foreach ($appTierService in (Get-AppTierServices)) { + $appName = $appTierService.Name + $newUserName = $applicationsDictionary[$appName] -replace $DEFAULTVALUE,$podGMSAAccountParent + $appTierService.User = $newUserName + Write-Host "$($appTierService.Name) : $($appTierService.User)" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.Installation/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..5f79653 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Private/VariableDeclarations.ps1 @@ -0,0 +1 @@ +[System.Reflection.Assembly]::LoadWithPartialName("System.Xml") | Out-Null diff --git a/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.ps1 b/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.ps1 new file mode 100644 index 0000000..1328dfa --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.ps1 @@ -0,0 +1,187 @@ +function Add-OldHotfixPackagesToUninstallList { + + <# + .SYNOPSIS + Adds hotfix packages which need to be removed to the package uninstall list. + + .PARAMETER DependencyReleaseValue + The value of the orb release being classified. + + .PARAMETER PackageMetadata + Packages object to populate. + + .PARAMETER DebugMetadata + Meta object used to determine what goes where. + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$DependencyReleaseValue = "", + + [Parameter(Mandatory = $true)] + [PSObject] $PackageMetadata, + + [Parameter(Mandatory = $true)] + [PSObject] $DebugMetadata + ) + $loglead = Get-LogleadName + + $releaseVersion = $null + $releaseVersionIsCanonical = $false + + # We now run both orb and package deploys through here. So we handle messaging different based on which path we came from. + if ($PackageMetadata.ForceReinstallPackages) { + if (Test-StringIsNullOrWhiteSpace -Value $DependencyReleaseValue) { + Write-Host "##teamcity[message text='DependencyReleaseValue is NullOrWhiteSpace on an ORB deploy!' status='WARNING']" + } + + # matches + # release dot 4digits dot at-least-one-digit dot at-least-one-digit dot at-least-one-digit dot zip + # parens around (\d{4}\.\d+\.\d+\.\d+) "captures" that string that looks like YYYY.N.N.N or 2022.3.0.9 + # which is our Release Version Number, or OrbVersion, matching what Get-OrbVersion would return + $regexOrbReleaseFile = "release\.(\d{4}\.\d+\.\d+\.\d+)\.zip" + if ($DependencyReleaseValue -match $regexOrbReleaseFile) { + $releaseVersion = $Matches[1] + Write-Host "$loglead : Valid release version found in dependency parameters" + $PackageMetadata.InstalledOrbVersion = $releaseVersion + $releaseVersionIsCanonical = $true + } else { + Write-Host "##teamcity[message text='DependencyReleaseValue does not match release name format. Fallback to Get-OrbVersion from remote host' status='WARNING']" + } + } else { + Write-Host "$loglead : This is a package deploy, so we're using Get-OrbVersion from a remote host to determine what to do with hotfixes." + } + + $firstWebORBServer = $PackageMetadata.WebServers | Select-Object -First 1 + $firstAppORBServer = $PackageMetadata.AppServers | Select-Object -First 1 + $firstMicORBServer = $PackageMetadata.MicServers | Select-Object -First 1 + + $orbServersToCheck = @($firstWebORBServer, $firstAppORBServer, $firstMicORBServer) + + $foundAtLeastOneOrbServer = $false + + if($null -eq $DebugMetadata.ExistingInstalledHotfixes){ + $DebugMetadata.ExistingInstalledHotfixes = @() + } + + foreach ($server in $orbServersToCheck) { + Write-Host "$loglead : Testing $server for hotfix installs..." + if ( -NOT (Test-StringIsNullOrWhiteSpace -Value $server)) { + # The anticipated number of packages in this block is 0 or 1 according to business rules + # So while this may take a little time for the Invoke-Command to run, the resulting array values should be tiny + # That being said, expect the business rules of 0 or 1 to be broken, this could end up with many values + if (-NOT $releaseVersionIsCanonical) { + $PackageMetadata.InstalledOrbVersion = Get-OrbVersion -ComputerName $server + } + + if ( -NOT (Test-StringIsNullOrWhiteSpace -Value $PackageMetadata.InstalledOrbVersion)) { + $remoteHotfix = Invoke-Command -ComputerName $server -ScriptBlock { + return (Get-AllInstalledComponentsByType -ComparableComponentType Hotfix) + } + + if( -NOT (Test-IsCollectionNullOrEmpty -Collection $remoteHotfix)){ + $DebugMetadata.ExistingInstalledHotfixes += $remoteHotfix + } + } else { + Write-Host "##teamcity[message='Could not retrieve the ORB version as a valid value from $server - ORB Version (Get-OrbVersion) value was empty or missing.' status='WARNING']" + } + $foundAtLeastOneOrbServer = $true + } + } + + # Some things are more readable if we write out the values as variables rather than magic numbers + # Oh to have enums + $versionIsLessThanTarget = -1 + $versionIsSameAsTarget = 0 + $versionIsGreaterThanTarget = 1 + + # Detect that this hotfix targets a valid version, aka: the version of ORB on or after-which, the hotfix is not required + <# + By way of example + 2021.5.0.10 has a bug + The bug is fixed in 2021.5.0.26 + The manifest for the hotfix package would state 2021.5.0.26 + The version of ORB is currently deployed at 2021.5.0.10 + #> + foreach ($existingHotfix in $DebugMetadata.ExistingInstalledHotfixes) { + Write-Host "$loglead : Existing Hotfix found: $($existingHotfix.PackageName)" + $compareSemverResult = Compare-SemVer -Version1 $existingHotfix.Manifest.hotfixManifest.fixedInOrbVersion -Version2 $PackageMetadata.InstalledOrbVersion + + switch ($compareSemverResult) { + $versionIsLessThanTarget { $DebugMetadata.UninstallExistingHotfixPackageNames += $existingHotfix.PackageName } + $versionIsSameAsTarget { $DebugMetadata.UninstallExistingHotfixPackageNames += $existingHotfix.PackageName } + $versionIsGreaterThanTarget { <# Do nothing, this will get reinstalled if it should be reinstalled at this point #> } + } + } + + if ( -NOT (Test-IsCollectionNullOrEmpty -Collection $DebugMetadata.UninstallExistingHotfixPackageNames)) { + Write-Host "$loglead : Found hotfixes to be uninstalled: $($DebugMetadata.UninstallExistingHotfixPackageNames -join ",")" + } + + if (-NOT $foundAtLeastOneOrbServer) { + Write-Host "##teamcity[message='Could not calculate the ORB version as no orb servers were resolved (web, app)' status='WARNING']" + } + + $allWebTierHotfixPackages = @() + $allAppTierHotfixPackages = @() + $allMicTierHotfixPackages = @() + + # Slice the packages out of the arrays so we can move them to the uninstall lists + foreach ($hotfixPackageName in $DebugMetadata.UninstallExistingHotfixPackageNames) { + # we use this to pull the hydrated object that has already be interogated and classified + Write-Host "$loglead : Getting Already Installed hotfixes." + [array]$allWebTierHotfixPackages += $DebugMetadata.WebServerPackages.Where( { $_.Name -eq $hotfixPackageName }) + [array]$allAppTierHotfixPackages += $DebugMetadata.AppServerPackages.Where( { $_.Name -eq $hotfixPackageName }) + [array]$allMicTierHotfixPackages += $DebugMetadata.MicServerPackages.Where( { $_.Name -eq $hotfixPackageName }) + + # The following lines are to slice out the hot fix package from the install list + # Something in this block (presumably the MicPackagesToInstall collection) below is coming in as a [Deserialized.System.Management.Automation.PSCustomObject]. + # PSCustomObjects don't know what .Where is, so this throws. + # The parens/[array] typecasting are an attempt to address that. + Write-Host "$loglead : Removing old hotfixes from install lists." + [array]$PackageMetadata.WebPackagesToInstall = ([array]$PackageMetadata.WebPackagesToInstall).Where( { $_.Name -ne $hotfixPackageName }) + [array]$PackageMetadata.AppPackagesToInstall = ([array]$PackageMetadata.AppPackagesToInstall).Where( { $_.Name -ne $hotfixPackageName }) + [array]$PackageMetadata.MicPackagesToInstall = ([array]$PackageMetadata.MicPackagesToInstall).Where( { $_.Name -ne $hotfixPackageName }) + } + + $allWebTierHotfixPackages.ForEach( { + Add-Member -InputObject $_ -NotePropertyName "SkipUninstallScripts" -NotePropertyValue $false -Force + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "ORB_HOTFIX_OUTDATED" -Force + }) + + $allAppTierHotfixPackages.ForEach( { + Add-Member -InputObject $_ -NotePropertyName "SkipUninstallScripts" -NotePropertyValue $false -Force + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "ORB_HOTFIX_OUTDATED" -Force + }) + + # If it's in the user-supplied uninstall list, we should not add it to the uninstall list, it will just get uninstalled for us, which is fine + # Adding it when it already exists can trip the duplicates-in-list state + foreach ($package in $allWebTierHotfixPackages) { + if ($PackageMetadata.WebPackagesToUninstall.Name -notcontains $package.Name) { + # Only add it to the uninstall list if it wasn't given to us by the user already + if (Test-IsCollectionNullOrEmpty -Collection $PackageMetadata.WebPackagesToUninstall) { $PackageMetadata.WebPackagesToUninstall = @() } + $PackageMetadata.WebPackagesToUninstall += $package + } + } + + foreach ($package in $allAppTierHotfixPackages) { + if ($PackageMetadata.AppPackagesToUninstall.Name -notcontains $package.Name) { + # Only add it to the uninstall list if it wasn't given to us by the user already + if (Test-IsCollectionNullOrEmpty -Collection $PackageMetadata.AppPackagesToUninstall) { $PackageMetadata.AppPackagesToUninstall = @() } + $PackageMetadata.AppPackagesToUninstall += $package + } + } + + foreach($package in $allMicTierHotfixPackages) { + if ($PackageMetadata.MicPackagesToUninstall.Name -notcontains $package.Name) { + # Only add it to the uninstall list if it wasn't given to us by the user already + if (Test-IsCollectionNullOrEmpty -Collection $PackageMetadata.MicPackagesToUninstall) { $PackageMetadata.MicPackagesToUninstall = @() } + $PackageMetadata.MicPackagesToUninstall += $package + } + } + + return $PackageMetadata, $DebugMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.tests.ps1 new file mode 100644 index 0000000..c273ec4 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Add-OldHotfixPackagesToUninstallList.tests.ps1 @@ -0,0 +1,903 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +# Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Add-OldHotfixPackagesToUninstallList" { + $defaultOrbValue = "2022.1.0.0" + $defaultReleaseValue = "2022.1.0.0" + $defaultFixedInValue = "2022.1.0.0" + + Mock -ModuleName $moduleForMock -CommandName Write-Host + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "UnitTest" } + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { return $defaultOrbValue } + + $fakeHotfix = New-DummyPackageInstallationData -PackageName "fake.hotfix" -PackageVersion "1.1.0" -IsFullScale -IsHotfix + + $webDummyPackage = New-DummyPackageInstallationData -PackageName "web.dummy" -PackageVersion "1.1.0" -IsWebOnly + $appDummyPackage = New-DummyPackageInstallationData -PackageName "app.dummy" -PackageVersion "1.1.0" -IsAppOnly + + # Define wrapper objects + $packageData = New-PackageMetadataObject + + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.WebPackagesToInstall += $fakeHotfix + $packageData.AppPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + Context "When DependencyReleaseValue is invalid and it's an orb release" { + It "Warns the User" { + $packageData.ForceReinstallPackages = $true + Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue "WHARRGARBL" -PackageMetadata $packageData -DebugMetadata $debugMetadata + + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { $Object -eq "##teamcity`[message text='DependencyReleaseValue does not match release name format. Fallback to Get-OrbVersion from remote host' status='WARNING'`]" } + } + } + + Context "When it's a package release" { + It "Doesn't Warn the User About DependencyReleaseValue" { + $packageData.ForceReinstallPackages = $false + Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue "" -PackageMetadata $packageData -DebugMetadata $debugMetadata + + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 0 -Exactly -Scope It -ParameterFilter { $Object -eq "##teamcity`[message text='DependencyReleaseValue does not match release name format. Fallback to Get-OrbVersion from remote host' status='WARNING'`]" } + } + } + + Context "When both app and web orb servers are Null" { + It "Warns the User" { + Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { $Object -eq "##teamcity`[message='Could not calculate the ORB version as no orb servers were resolved (web, app)' status='WARNING'`]" } + } + } + + # Define web, app servers for future tests + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + # We're assuming this is null/unretrievable for some reason on both app and web servers for test simplicity. + Context "When InstalledOrbVersion is Null" { + It "Warns the User" { + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { return $null } + + Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue "bad.Release" -PackageMetadata $packageData -DebugMetadata $debugMetadata + + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 2 -Exactly -Scope It -ParameterFilter { $Object -like "##teamcity``[message='Could not retrieve the ORB version as a valid value from * - ORB Version (Get-OrbVersion) value was empty or missing.' status='WARNING'``]" } + } + } + +#region Tests for hotfix on all server types + Context "When The Existing Hotfix Version is Less Than The Target On All Server Types" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + $packageData.MicServers = @("micfake1.fh.local", "micfake2.fh.local") + + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $appDummyPackage + + $packageData.WebPackagesToInstall += $fakeHotfix + $packageData.AppPackagesToInstall += $fakeHotfix + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + $debugMetadata.AppServerPackages += $fakeHotfix + $debugMetadata.MicServerPackages += $fakeHotfix + + # This returns a hotfix package which is installed on an orb server. + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "2010.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Contain $fakeHotfix + $outputMetaData.AppPackagesToUninstall | Should -Contain $fakeHotfix + $outputMetaData.MicPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Not -Contain $fakeHotfix + $outputMetaData.WebPackagesToInstall | Should -Not -Contain $fakeHotfix + $outputMetaData.MicPackagesToInstall | Should -Not -Contain $fakeHotfix + } + } + + Context "When The Existing Hotfix Version is Equal To The Target On All Server Types" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + $packageData.MicServers = @("micfake1.fh.local", "micfake2.fh.local") + + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $appDummyPackage + + $packageData.WebPackagesToInstall += $fakeHotfix + $packageData.AppPackagesToInstall += $fakeHotfix + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + $debugMetadata.AppServerPackages += $fakeHotfix + $debugMetadata.MicServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = $defaultFixedInValue } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Contain $fakeHotfix + $outputMetaData.AppPackagesToUninstall | Should -Contain $fakeHotfix + $outputMetaData.MicPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Not -Contain $fakeHotfix + $outputMetaData.WebPackagesToInstall | Should -Not -Contain $fakeHotfix + $outputMetaData.MicPackagesToInstall | Should -Not -Contain $fakeHotfix + } + } + + Context "When The Existing Hotfix Version is Greater Than The Target On All Server Types" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + $packageData.MicServers = @("micfake1.fh.local", "micfake2.fh.local") + + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $appDummyPackage + + $packageData.WebPackagesToInstall += $fakeHotfix + $packageData.AppPackagesToInstall += $fakeHotfix + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + $debugMetadata.AppServerPackages += $fakeHotfix + $debugMetadata.MicServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "9999.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + It "Does Not Tell The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 0 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Does Not Add The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Not -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Not -Contain $fakeHotfix + $outputMetaData.AppPackagesToUninstall | Should -Not -Contain $fakeHotfix + $outputMetaData.MicPackagesToUninstall | Should -Not -Contain $fakeHotfix + } + + It "Does Not Remove The Package From The Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Contain $fakeHotfix + $outputMetaData.WebPackagesToInstall | Should -Contain $fakeHotfix + $outputMetaData.MicPackagesToInstall | Should -Contain $fakeHotfix + } + } +#endregion Tests for hotfix on all server types + +# #region Tests for hotfix only on app servers +Context "When The Existing Hotfix Version is Less Than The Target On App Servers" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.AppPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.AppServerPackages += $fakeHotfix + + # This returns a hotfix package which is installed on an orb server. + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*webfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "2010.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.AppPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The App Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Not -Contain $fakeHotfix + } +} + +Context "When The Existing Hotfix Version is Equal To The Target On App Servers" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.AppPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.AppServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*webfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = $defaultFixedInValue } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.AppPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The App Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Not -Contain $fakeHotfix + } + +} + +Context "When The Existing Hotfix Version is Greater Than The Target On App Servers" { + + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.AppPackagesToInstall += $appDummyPackage + $packageData.AppPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.AppServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*webfake1*" -or $ComputerName -like "*micfake1*"} + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "9999.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + It "Does Not Tell The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 0 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Does Not Add The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Not -Contain "fake.hotfix" + + $outputMetaData.AppPackagesToUninstall | Should -Not -Contain $fakeHotfix + } + + It "Does Not Remove The Package From The App Install List" { + $outputMetaData.AppPackagesToInstall | Should -Not -Be $null + + $outputMetaData.AppPackagesToInstall | Should -Contain $fakeHotfix + } +} +# #endregion Tests for hotfix only on app servers + +# #region Tests for hotfix only on mic servers +Context "When The Existing Hotfix Version is Less Than The Target On Mic Servers" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + $packageData.MicServers = @("micfake1.fh.local", "micfake2.fh.local") + + $packageData.MicPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.MicServerPackages += $fakeHotfix + + # This returns a hotfix package which is installed on an orb server. + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter { ($ComputerName -like "*webfake1*") -or ($ComputerName -like "*appfake1*") } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "2010.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*micfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.MicPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The Mic Install List" { + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + + $outputMetaData.MicPackagesToInstall | Should -Not -Contain $fakeHotfix + } +} + +Context "When The Existing Hotfix Version is Equal To The Target On Mic Servers" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + $packageData.MicServers = @("micfake1.fh.local", "micfake2.fh.local") + + $packageData.MicPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.MicServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*webfake1*" -or $ComputerName -like "*appfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = $defaultFixedInValue } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*micfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.MicPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The Mic Install List" { + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + + $outputMetaData.MicPackagesToInstall | Should -Not -Contain $fakeHotfix + } +} + +Context "When The Existing Hotfix Version is Greater Than The Target On Mic Servers" { + + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.MicPackagesToInstall += $appDummyPackage + $packageData.MicPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.MicServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter { $ComputerName -like "*webfake1*" -or $ComputerName -like "*appfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "9999.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*micfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + It "Does Not Tell The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 0 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Does Not Add The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Not -Contain "fake.hotfix" + + $outputMetaData.MicPackagesToUninstall | Should -Not -Contain $fakeHotfix + } + + It "Does Not Remove The Package From The Mic Install List" { + $outputMetaData.MicPackagesToInstall | Should -Not -Be $null + + $outputMetaData.MicPackagesToInstall | Should -Contain $fakeHotfix + } +} +# #endregion Tests for hotfix only on Mic servers + +#region Tests for hotfix only on web servers +Context "When The Existing Hotfix Version is Less Than The Target On Web Servers" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.WebPackagesToUninstall += $fakeHotfix + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.WebPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + # This returns a hotfix package which is installed on an orb server. + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "2010.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*webfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + # this test is when the same hotfix is installed on both app and web + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The App Install List" { + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + + $outputMetaData.WebPackagesToInstall | Should -Not -Contain $fakeHotfix + } +} + +Context "When The Existing Hotfix Version is Equal To The Target On All Server Types" { + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.WebPackagesToInstall += $webDummyPackage + $packageData.WebPackagesToInstall += $fakeHotfix + $packageData.WebPackagesToUninstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = $defaultFixedInValue } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*webfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + It "Tells The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Adds The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Contain $fakeHotfix + } + + It "Removes The Package From The Install Web List" { + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + + $outputMetaData.WebPackagesToInstall | Should -Not -Contain $fakeHotfix + } + +} + +Context "When The Existing Hotfix Version is Greater Than The Target On Web Servers" { + + # Define wrapper objects + $packageData = New-PackageMetadataObject + $packageData.WebServers = @("webfake1.fh.local", "webfake2.fh.local") + $packageData.AppServers = @("appfake1.fh.local", "appfake2.fh.local") + + $packageData.WebPackagesToInstall += $webDummyPackage + + $packageData.WebPackagesToInstall += $fakeHotfix + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + UninstallExistingHotfixPackageNames = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $debugMetadata.WebServerPackages += $fakeHotfix + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return $null + } -ParameterFilter {$ComputerName -like "*appfake1*" } + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { + return [PSCustomObject]@{ + PackageName = "fake.hotfix" + ManifestPath = "TestDrive:\FakeManifest.Xml" + Manifest = @{ + "hotfixManifest" = @{fixedInORBVersion = "9999.0.0.0" } + "general" = @{ + creatorCode = "Alkami" + element = "Alkami.Hotfix.UnitTest" + componentType = "Hotfix" + } + "version" = 1.0 + } + } + } -ParameterFilter {$ComputerName -like "*webfake1*" } + + $outputMetaData, $outputDebugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $defaultReleaseValue -PackageMetadata $packageData -DebugMetadata $debugMetadata + + It "Does Not Tell The User It Will Uninstall The Hotfix" { + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 0 -Exactly -Scope Context -ParameterFilter { $Object -like "*Found hotfixes to be uninstalled*" } + } + + It "Does Not Add The Package To The Uninstall List"{ + $outputDebugMetadata.UninstallExistingHotfixPackageNames | Should -Not -Contain "fake.hotfix" + + $outputMetaData.WebPackagesToUninstall | Should -Not -Contain $fakeHotfix + } + + It "Does Not Remove The Package From The Install List" { + $outputMetaData.WebPackagesToInstall | Should -Not -Be $null + + $outputMetaData.WebPackagesToInstall | Should -Contain $fakeHotfix + } +} +#endregion Tests for hotfix only on web servers +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.ps1 b/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.ps1 new file mode 100644 index 0000000..e0e4fc2 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.ps1 @@ -0,0 +1,53 @@ +function Add-OverflowCustomAttribute { +<# +.SYNOPSIS + Adds Overflow: true or false as custom node attributes in newrelic-infra.yml + +.DESCRIPTION + If a server is an overflow server, Perf wants to know via new relic custom attribute. + This affects the server that this function is being run on. + +.PARAMETER IsOverflow + IsOverflow sets the value to true or false + +.EXAMPLE + Add-OverflowCustomAttribute -IsOverflow $true +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [bool]$IsOverflow + ) + + $logLead = Get-LogLeadName + + $isDirty = $false + Import-Module powershell-yaml + + $Path = Get-NewRelicYamlPath + # Returning/stopping exactly where the error is + if (Test-StringIsNullOrWhitespace $Path) { return } + + $fileContent = Get-Content $Path -Raw + $yaml = ConvertFrom-Yaml -Yaml $fileContent -Ordered + + # This will add the custom_attribute field if it isn't already present + # Otherwise it will update the Overflow value + if (!($yaml.custom_attributes)) { + $yaml.custom_attributes = @{Overflow = $IsOverflow } + $isDirty = $true + } elseif ($yaml.custom_attributes.Overflow -ne $IsOverflow) { + $yaml.custom_attributes.Overflow = $IsOverflow + $isDirty = $true + } + + if ($isDirty) { + Write-Host "$loglead : Setting Overflow Attribute to $IsOverflow" + Write-Host "$loglead : Saving to path: $Path" + ConvertTo-Yaml -Data $yaml -Outfile $Path -Force + } else { + Write-Host "$loglead : Nothing to change in the config file" + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.tests.ps1 new file mode 100644 index 0000000..e9e3131 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Add-OverflowCustomAttribute.tests.ps1 @@ -0,0 +1,71 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Add-OverflowCustomAttribute" { + Mock -ModuleName $moduleForMock -CommandName Get-Content + Mock -ModuleName $moduleForMock -CommandName Test-Path + Mock -ModuleName $moduleForMock -CommandName Write-Warning + Mock -ModuleName $moduleForMock -CommandName Write-Host + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName + Mock -ModuleName $moduleForMock -CommandName Import-Module + Mock -ModuleName $moduleForMock -CommandName ConvertTo-Yaml + Mock -ModuleName $moduleForMock -CommandName Get-NewRelicYamlPath -MockWith { "C:\Program Files\New Relic\newrelic-infra\newrelic-infra.yml" } + + Context "Updates custom attributes" { + + It "Overflow value was false" { + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { return @{ custom_attributes = @{ Overflow = $true } } } + Add-OverflowCustomAttribute -IsOverflow $false + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $false } + } + It "Overflow value was true" { + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { return @{ custom_attributes = @{ Overflow = $false } } } + Add-OverflowCustomAttribute -IsOverflow $true + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $true } + } + } + + Context "Adds custom attributes and Overflow when only custom_attributes is present" { + + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { return @{ custom_attributes = @{ Pod = 17 } } } + + It "Overflow value was true" { + Add-OverflowCustomAttribute -IsOverflow $true + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $true } + } + It "Overflow value was false" { + Add-OverflowCustomAttribute -IsOverflow $false + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $false } + } + } + + Context "Adds custom attributes and Overflow when custom_attributes and Overflow are not present" { + + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { return @{ } } + + It "Overflow value was false" { + Add-OverflowCustomAttribute -IsOverflow $false + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $false } + } + It "Overflow Value was true" { + Add-OverflowCustomAttribute -IsOverflow $true + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $data.custom_attributes.Overflow -eq $true } + } + } + + Context "No file changes if no file changes are needed" { + + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { return @{ custom_attributes = @{ Pod = 17; Overflow = $false } } } + + It "No file change if IsOverflow is the same value in file" { + Add-OverflowCustomAttribute -IsOverflow $false + Assert-MockCalled -CommandName ConvertTo-Yaml -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Copy-NewRelicCustomInstrumentationFiles.ps1 b/Modules/Alkami.DevOps.Installation/Public/Copy-NewRelicCustomInstrumentationFiles.ps1 new file mode 100644 index 0000000..a680ad2 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Copy-NewRelicCustomInstrumentationFiles.ps1 @@ -0,0 +1,22 @@ +function Copy-NewRelicCustomInstrumentationFiles { +<# + .SYNOPSIS + Copies the New Relic .NET Agent Custom Instrumentation Files to the NR Extensions Folder +#> + [CmdletBinding()] + param( + + ) + $logLead = (Get-LogLeadName); + + ## Get the custom path from the current folder -> NewRelicCustomInstrumentation + $customInstrumentationPath = Join-Path $PSScriptRoot "NewRelicCustomInstrumentation" + if (Test-Path $customInstrumentationPath) { + $nrExtensionsPath = "C:\ProgramData\New Relic\.NET Agent\Extensions" + Write-Output ("$logLead : Copying Custom Instrumentation Files from {0} to {1}" -f $customInstrumentationPath, $nrExtensionsPath) + + Get-ChildItem $customInstrumentationPath | Select-Object -ExpandProperty FullName | Copy-Item -Destination $nrExtensionsPath -Force + } else { + Write-Warning ("$logLead : Skipping copy of custom instrumentation files from {0}" -f $customInstrumentationPath) + } +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.Tests.ps1 new file mode 100644 index 0000000..87d89b8 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.Tests.ps1 @@ -0,0 +1,109 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe "Get-BadPackages" { + + Mock -CommandName Write-Host -MockWith {} + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Write-Error -MockWith {} + + $webPackage1 = New-DummyPackageInstallationData -PackageName "web.package1" -PackageVersion "1.0.0" -IsWebOnly + $webPackage2 = New-DummyPackageInstallationData -PackageName "web.package2" -PackageVersion "1.0.0" -IsWebOnly + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsAppOnly + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsAppOnly + $micPackage1 = New-DummyPackageInstallationData -PackageName "mic.package1" -PackageVersion "1.0.0" -IsMicOnly + $micPackage2 = New-DummyPackageInstallationData -PackageName "mic.package2" -PackageVersion "1.0.0" -IsMicOnly + + $appPackageArray = @($appPackage1, $appPackage2) + $webPackageArray = @($webPackage1, $webPackage2) + $micPackageArray = @($micPackage1, $micPackage2) + + $appServerPackageArray = @($appPackage1, $appPackage2, $webPackage1) + $webServerPackageArray = @($webPackage1, $webPackage2, $appPackage1) + $micServerPackageArray = @($micPackage1, $micPackage2, $webPackage1) + $fabServerPackageArray = @($micPackage1, $micPackage2, $webPackage1) + + # Define wrapper objects + $packageData = New-PackageMetadataObject + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + } + + # Populate wrapper objects. + $packageData.AppPackagesToInstall = $appPackageArray + $packageData.WebPackagesToInstall = $webPackageArray + $packageData.MicPackagesToInstall = $micPackageArray + $debugMetadata.WebServerPackages = $webServerPackageArray + $debugMetadata.AppServerPackages = $appServerPackageArray + $debugMetadata.MicServerPackages = $micServerPackageArray + $debugMetadata.FabServerPackages = $fabServerPackageArray + + $debugMetadata.ClassifiedPackagesMap["web.package1"] = $webPackage1 + $debugMetadata.ClassifiedPackagesMap["web.package2"] = $webPackage2 + $debugMetadata.ClassifiedPackagesMap["app.package1"] = $appPackage1 + $debugMetadata.ClassifiedPackagesMap["app.package2"] = $appPackage2 + $debugMetadata.ClassifiedPackagesMap["mic.package1"] = $micPackage1 + $debugMetadata.ClassifiedPackagesMap["mic.package2"] = $micPackage2 + + Context "When Debug MetaData Has WebServerPackages" { + It "Marks Packages To Be Uninstalled From Web Servers" { + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadWebPackagesToUninstall | Should -Not -BeNullOrEmpty + } + } + + Context "When Debug MetaData Has AppServerPackages" { + It "Marks Packages To Be Uninstalled From App Servers" { + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadAppPackagesToUninstall | Should -Not -BeNullOrEmpty + } + + It "Sets the ActionType to Uninstall"{ + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadAppPackagesToUninstall.ActionType | Should -BeLikeExactly "Uninstall" + } + + It "Sets the ActionReason to Wrong_Host_Type"{ + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadAppPackagesToUninstall.ActionReason | Should -BeLikeExactly "Wrong_Host_Type" + } + } + Context "When Debug MetaData Has MicServerPackages" { + It "Marks Packages To Be Uninstalled From Mic Servers" { + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadMicPackagesToUninstall | Should -Not -BeNullOrEmpty } + } + Context "When Debug MetaData Has FabServerPackages" { + It "Marks Packages To Be Uninstalled From Mic Servers" { + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadMicPackagesToUninstall | Should -Not -BeNullOrEmpty + } + + It "Does Not Mark Packages To Be Uninstalled From Fab Servers" { + $packageData = Get-BadPackages $DebugMetadata $packageData + + $packageData.BadFabPackagesToUninstall | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.ps1 new file mode 100644 index 0000000..621c934 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-BadPackages.ps1 @@ -0,0 +1,105 @@ +function Get-BadPackages { + <# + .SYNOPSIS + Determine if some packages are on the wrong servers. Utilized by Classify-Packages. + + .PARAMETER DebugMetadata + Meta object used to determine what goes where. + + .PARAMETER PackageMetadata + Packages object to populate. + #> + [CmdletBinding()] + param( + $DebugMetadata, + $PackageMetadata + ) + # Produce a list of bad packages on the wrong servers, to be uninstalled. + if (!(Test-IsCollectionNullOrEmpty $DebugMetadata.WebServerPackages)) { + $packages = @() + foreach($package in $DebugMetadata.WebServerPackages) { + if($null -ne $package) { + ## if the package was found on the web server but it wasn't supposed to be on the web, remove it + $packageNameLower = $package.Name.ToLower() + $foundPackage = $DebugMetadata.ClassifiedPackagesMap.$packageNameLower + if (($null -ne $foundPackage) -and (!$foundPackage.InstallToWeb)) { + $packages += $foundPackage + $packageMetadata.HasBadPackages = $true + } + } + } + $packages.ForEach({ + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "Wrong_Host_Type" -Force + }) + $PackageMetadata.BadWebPackagesToUninstall = $packages + } + + if (!(Test-IsCollectionNullOrEmpty $DebugMetadata.AppServerPackages)) { + $packages = @() + foreach ($package in $DebugMetadata.AppServerPackages) { + if ($null -ne $package) { + ## if the package was found on the app server but it wasn't supposed to be on the app, remove it + $packageNameLower = $package.Name.ToLower() + $foundPackage = $DebugMetadata.ClassifiedPackagesMap.$packageNameLower + if (($null -ne $foundPackage) -and (!$foundPackage.InstallToApp)) { + $packages += $foundPackage + $PackageMetadata.HasBadPackages = $true + } + } + } + $packages.ForEach({ + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "Wrong_Host_Type" -Force + }) + $PackageMetadata.BadAppPackagesToUninstall = $packages + } + + if (!(Test-IsCollectionNullOrEmpty $DebugMetadata.MicServerPackages)) { + $packages = @() + foreach($package in $DebugMetadata.MicServerPackages) { + if ($null -ne $package) { + ## if the package was found on the mic server but it wasn't supposed to be on the mic, remove it + $packageNameLower = $package.Name.ToLower() + $foundPackage = $DebugMetadata.ClassifiedPackagesMap.$packageNameLower + if (($null -ne $foundPackage) -and (!$foundPackage.InstallToMic)) { + $packages += $foundPackage + $PackageMetadata.HasBadPackages = $true + } + } + } + $packages.ForEach({ + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "Wrong_Host_Type" -Force + }) + $PackageMetadata.BadMicPackagesToUninstall = $packages + } + + if (!(Test-IsCollectionNullOrEmpty $DebugMetadata.FabServerPackages)) { + $packages = @() + foreach ($package in $DebugMetadata.FabServerPackages) { + if ($null -ne $package) { + ## if the package was found on the fab server but it wasn't supposed to be on the fab, remove it + $packageNameLower = $package.Name.ToLower() + $foundPackage = $DebugMetadata.ClassifiedPackagesMap.$packageNameLower + if (($null -ne $foundPackage) -and (!$foundPackage.InstallToFab)) { + $packages += $foundPackage + $PackageMetadata.HasBadPackages = $true + } + } + } + $packages.ForEach({ + Add-Member -InputObject $_ -NotePropertyName "ActionType" -NotePropertyValue "Uninstall" -Force + Add-Member -InputObject $_ -NotePropertyName "ActionReason" -NotePropertyValue "Wrong_Host_Type" -Force + }) + $PackageMetadata.BadFabPackagesToUninstall = $packages + } + + # Combine the mic/fab uninstalls into mics only. Mics/Fabs are mutually exclusive in the eyes of the deploy, might as well use one var. + $PackageMetadata.BadMicPackagesToUninstall = (Select-UniqueServerPackages @($PackageMetadata.BadMicPackagesToUninstall, $PackageMetadata.BadFabPackagesToUninstall)) + $PackageMetadata.BadFabPackagesToUninstall = @() + + # NOTE: The bad packages lists are added to the web/app/mic uninstall lists below, where the mic uninstalls are separate from the app installs. + + return $PackageMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-ClientWebSiteInformationFromDatabase.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-ClientWebSiteInformationFromDatabase.ps1 new file mode 100644 index 0000000..075ed92 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-ClientWebSiteInformationFromDatabase.ps1 @@ -0,0 +1,28 @@ +function Get-ClientWebSiteInformationFromDatabase { +<# +.SYNOPSIS + Fetches Client Website Information from Database. +#> + + [CmdletBinding()] + param() + # Todo - pull other machines in OU and determine which is an app server + # Execute the below on the remote server + + [hashtable[]]$clients += Get-CatalogsFromMaster + $urls = @() + + if ($clients.Count -eq 1) { + $ipstsUrl = Get-IPSTSUrlFromClient $clients + $urls += @{Client = $clients.Signature; Admin = $clients.AdminSignature; IPSTS = $ipstsUrl} + } + else { + foreach ($client in $clients) { + $ipstsUrl = Get-IPSTSUrlFromClient $client + $urls += @{Key = $client.Name; Client = $client.Signature; Admin = $client.AdminSignature; IPSTS = $ipstsUrl} + } + } + + return $urls +} + diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.Tests.ps1 new file mode 100644 index 0000000..af91a82 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.Tests.ps1 @@ -0,0 +1,103 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe "Get-EnvironmentData" { + $fakeDotNetConfigPath = Join-Path -Path $TestDrive -ChildPath "machine.config" + # Return each of the settings based on parameter filters + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Mock -CommandName Get-UncPath -ModuleName $moduleForMock -MockWith { return $fakeDotNetConfigPath } + Mock -CommandName Read-XmlFile -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Invoke-Parallel -ModuleName $moduleForMock -MockWith {} + + Context "When Successfully Getting EnvironmentData from AppSettings" { + $packageData = @{} + $packageData.DisableMicroserviceNewRelic = $true + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + + # these tests were essentially "does Get-EnvironmentThing work?" + # there's no business logic to them + # It "It Gets EnvironmentName" { + # $data = Get-EnvironmentData $packageData + + # $data.EnvironmentName | Should -Match "UnitTest.Env" + # } + + # It "It Gets EnvironmentType" { + # $data = Get-EnvironmentData $packageData + + # $data.EnvironmentType | Should -Match "UnitTest.Type" + # } + + # It "It Gets EnvironmentNameSafeDesignation" { + # $data = Get-EnvironmentData $packageData + + # $data.EnvironmentNameSafeDesignation | Should -Match "UTT" + # } + + # It "It Gets EnvironmentHosting" { + # $data = Get-EnvironmentData $packageData + + # $data.EnvironmentHosting | Should -Match "local" + # } + } + + Context "When Getting Data from Tags" { + $packageData = @{} + $packageData.AppServers = @("APP123TEST.fh.local") + $packageData.DisableMicroserviceNewRelic = $true + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + + Mock Get-InstanceTags -MockWith { # return tags + $tag1 = @{ + PSComputerName = "Test.Machine" + Key = "alk:env" + Value = "dev" + } + + $tag2 = @{ + PSComputerName = "Test.Machine" + Key = "alk:designation" + Value = "FAKEVALUE" + } + return @($tag1, $tag2) + } + + Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { + return "designation" + } + + # How was this mocked? It's a degree of separation away. It shouldnot have been being used.∫ + # Mock Get-AppSetting -MockWith {return "UnitTest.Env"} -ParameterFilter {$Key -eq "Environment.Name"} + # Mock Get-AppSetting -MockWith {return "UnitTest.Type"} -ParameterFilter {$Key -eq "Environment.Type"} + # Mock Get-AppSetting -MockWith {return "local"} -ParameterFilter {$Key -eq "Environment.Hosting"} + # No available value locally + Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null } -ParameterFilter { $Key -eq "Environment.NameSafeDesignation" } + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + It "It Gets EnvironmentNameSafeDesignation" { + $data = Get-EnvironmentData $packageData + + $data.EnvironmentNameSafeDesignation | Should -Match "FAKEVALUE" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.ps1 new file mode 100644 index 0000000..ee3bed6 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-EnvironmentData.ps1 @@ -0,0 +1,136 @@ +function Get-EnvironmentData { + <# + .SYNOPSIS + Get basic data about the environment. Called by Get-ServerPackageInformation as part of the Classify_Packages part of the Deploy pipeline + + .PARAMETER PackageMetadata + Packages object to populate + + .LINK + Get-ServerPackageInformation + + .LINK + Test-IsEclairInstalled + #> + + [CmdletBinding()] + param( + [PSObject]$PackageMetadata + ) + + $loglead = Get-LogLeadName + + $PackageMetadata.ServersToQuery = ( + @($PackageMetadata.WebServers) + + @($PackageMetadata.AppServers) + + @($PackageMetadata.MicServers) + + @($PackageMetadata.SelectedFabServer) + ) | Where-Object { $_ } + + $serverToQuery = $PackageMetadata.ServersToQuery | Select-Object -First 1 + + # Read the machine.config into a variable to avoid going across the network to read + # it every time we call Get-AppSetting... which we were doing before... :/ + $dncPath = Get-DotNetConfigPath + $dncUncPath = Get-UncPath -ComputerName $serverToQuery -filePath $dncPath -IgnoreLocalPaths + $machineConfigContent = Read-XMLFile -xmlPath $dncUncPath + + # Map from $packageMetadata KEY to machine.config KEY + $envDataKeyMap = @{ + EnvironmentName = "Environment.Name" + EnvironmentType = "Environment.Type" + EnvironmentHosting = "Environment.Hosting" + EnvironmentNameSafeDesignation = "Environment.NameSafeDesignation" + } + + foreach ($envDataKey in $envDataKeyMap.keys) { + Write-Host "$loglead : deployData Key: $envDataKey" + + $mcKey = $envDataKeyMap[$envDataKey] + Write-Host "$loglead : Machine config key: $mcKey" + + $mcValue = Get-AppSetting -Key $mcKey -XmlDocument $machineConfigContent -SuppressWarnings + Write-Host "$loglead : Machine config value: $mcValue" + + $packageMetadata.$envDataKey = $mcValue + } + + + if ($PackageMetadata.EnvironmentNameSafeDesignation) { + Write-Host "$loglead : EnvironmentNameSafeDesignation: $($PackageMetadata.EnvironmentNameSafeDesignation)" + } else { + Write-Warning "$loglead : NameSafeDesignation MISSING from the machine.config. Getting from tags..." + + $tags = Get-InstanceTags -ServerToTest $serverToQuery + + foreach ($tag in $tags) { + if ($tag.Key -eq "alk:env") { + $designation = Get-DesignationTagNameByEnvironment $tag.Value + } + } + if ($designation) { + foreach ($tag in $tags) { + if ($tag.Key -eq "alk:$designation") { + $PackageMetadata.EnvironmentNameSafeDesignation = $tag.Value.Replace('.', '-').Trim().ToLower() + } + } + } + + if (!$designation -or !$PackageMetadata.EnvironmentNameSafeDesignation) { + Write-Error "$loglead : Did not find a Designation tag. This server ($serverToQuery) is missing both machine.config values and tags. Investigation is required." + } + } + + if ($PackageMetadata.EnvironmentHosting) { + Write-Host "$loglead : EnvironmentHosting: $($PackageMetadata.EnvironmentHosting)" + } else { + Write-Warning "$loglead : Could not obtain a EnvironmentHosting. This will result in no Infrastructure Migrations being run." + } + + #region IsEclairInstalled + $sbIsEclairInstalled = { + param($server) + try { + $results = @{ + Hostname = $server + IsEclairInstalled = $false + Error = $null + Success = $false + } + $ei = Test-IsEclairInstalled -ComputerName $server + $results.IsEclairInstalled = $ei + $results.Success = $true + } catch { + $ex = $_ + $msg = $_.Exception.Message + Write-Warning "$loglead : There was an error testing for Eclair" + Write-Warning "$msg" + $results.Error = $ex + $results.Success = $false + } + return $results + } + + $ieiResults = Invoke-Parallel -objects $PackageMetadata.ServersToQuery -script $sbIsEclairInstalled + + $isEclairInstalledOnAllHosts = $ieiResults.IsEclairInstalled -notcontains $false + + Write-Host "$loglead : Is Eclair installed on all hosts: $isEclairInstalledOnAllHosts" + $PackageMetadata.IsEclairInstalledOnAllHosts = $isEclairInstalledOnAllHosts + + # Trim out crap from (RunspaceId, PSSourceJobInstanceId, etc.) ReceiveJob so the Classify Packages tests will pass + $trimmedResults = @() + foreach($result in $ieiResults) + { + $hash = @{} + foreach($enum in $result.GetEnumerator()) { + $hash.add($enum.key, $enum.value) + } + $trimmedResults += $hash + } + + $PackageMetadata.EclairInstallData = $trimmedResults + + #endregion IsEclairInstalled + return $PackageMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.Tests.ps1 new file mode 100644 index 0000000..c09e63a --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.Tests.ps1 @@ -0,0 +1,89 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe 'Get-MicroserviceNewRelicMapping'{ + # Silence messages. Comment these out if you're debugging tests. + Mock -CommandName Write-Host -MockWith {} + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Write-Error -MockWith {} + Mock -CommandName Set-Content -MockWith {} + Mock -CommandName Add-Member -MockWith {} + Mock -CommandName Join-Path -MockWith {return "TestDrive:\$ChildPath"} + Mock -CommandName Test-Path -MockWith {return $true} + Mock -CommandName Get-Content -MockWith { return @" +app.package1 +app.package2 +"@ + } + + Context 'When Some New Relic Monitoring Should Be Disabled' { + $packageData = New-PackageMetadataObject + $packageData.DisableMicroserviceNewRelic = $true + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + + It "Sets NR On/Off Status" { + $packages = Get-MicroserviceNewRelicMapping -BuildCheckoutDirectory "SomePath" -PackageMetadata $packageData + + $packages | Should -Not -BeNullOrEmpty + + Assert-MockCalled -CommandName Add-Member -Scope Context -Times 1 + } + } + + + Context 'When No New Relic Monitoring Should Be Disabled' { + $packageData = New-PackageMetadataObject + $packageData.DisableMicroserviceNewRelic = $false + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + + It "Does Not Modify New Relic Status On Any Packages"{ + $packages = Get-MicroserviceNewRelicMapping -BuildCheckoutDirectory "SomePath" -PackageMetadata $packageData + + $packages | Should -Not -BeNullOrEmpty + + Assert-MockCalled -CommandName Add-Member -Scope Context -Times 0 + } + } + + Context 'When NewRelicMicroServices.txt is not found' { + # No New Relic file: + Mock -CommandName Test-Path -MockWith {return $false} + Mock -CommandName Write-Warning -MockWith {} + + $packageData = New-PackageMetadataObject + $packageData.DisableMicroserviceNewRelic = $true + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + + It "Disables Everything"{ + + $packages = Get-MicroserviceNewRelicMapping -BuildCheckoutDirectory "SomePath" -PackageMetadata $packageData + + foreach ($package in $packages) { + $package.EnableNewRelic | Should -BeNullOrEmpty + } + Assert-MockCalled -CommandName Write-Warning -Scope Context -Times 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.ps1 new file mode 100644 index 0000000..78f03e9 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-MicroserviceNewRelicMapping.ps1 @@ -0,0 +1,43 @@ +function Get-MicroserviceNewRelicMapping { +<# +.SYNOPSIS + Determine which microservices should report to New Relic. Utilized by Classify-Packages. + +.PARAMETER BuildCheckoutDirectory + Directory where the code is checked out by TC. + +.PARAMETER PackageMetadata + Packages object to populate. +#> + + [CmdletBinding()] + param( + $BuildCheckoutDirectory, + [PSObject]$PackageMetadata + ) + # For each of the microservice packages, figure out if they will have new relic enabled or disabled. + # Build out the config checkout directory. + $configCheckoutDirectory = (Join-Path $BuildCheckoutDirectory "config-defaults") + + if($PackageMetadata.DisableMicroserviceNewRelic) { + Write-Host "Now determining which microservices need New Relic Enabled/Disabled" + + # Read in the package names to leave new-relic enabled for. + $newRelicServicePath = (Join-Path $configCheckoutDirectory "NewRelicMicroServices/NewRelicMicroServices.txt") + if(Test-Path $newRelicServicePath) { + $newRelicMicroservicesToLeaveEnabled = [array](Get-Content -Path $newRelicServicePath) + } else { + Write-Warning "Could not find NewRelicMicroServices.txt to leave new relic enabled. Assuming no microservices should have NewRelic enabled." + $newRelicMicroservicesToLeaveEnabled = $null; + } + foreach($package in $PackageMetadata.AppPackagesToInstall) { + if($package.IsMicroservice) { + $enableNewRelic = ($newRelicMicroservicesToLeaveEnabled -contains $package.Name) + Add-Member -InputObject $package -NotePropertyName "EnableNewRelic" -NotePropertyValue $enableNewRelic -Force + } + } + } else { + Write-Host "New Relic will be left enabled on microservices." + } + return $PackageMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicAccountDetails.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicAccountDetails.ps1 new file mode 100644 index 0000000..a0ce238 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicAccountDetails.ps1 @@ -0,0 +1,64 @@ +function Get-NewRelicAccountDetails { +<# +.SYNOPSIS + Returns the appropriate New Relic account (not Admin) API key and license key based on environment string +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("Environment")] + [string]$environmentKey + ) + + $logLead = (Get-LogLeadName); + + $specificAccountDetails = @( + # FI specific accounts. + @{EnvironmentRegex = "(AWS Production( Entrust)? 5)|(AWS Staging Lane FT\w+)"; APIKey = "5362c58099d6a19871a07f66b1ec50e2c00f66b4b69260e"; LicenseKey = "4e8134a277da93644407dc53931f81d99b69260e"; }, + @{EnvironmentRegex = "AWS Production( Entrust)? 9"; APIKey = "b84b09aabf2ebe0773009698205220e9d53063bf84d041f"; LicenseKey = "417a21abe2bd84ca46940efc9c93cf75584d041f"; }, + @{EnvironmentRegex = "(AWS Production( Entrust)? 10)|(AWS Staging Lane OTSStg)"; APIKey = "3d0346ec856cc9b02b3b0fcc481afc989a4f9941b28a80d"; LicenseKey = "d2eb268d151e57550de821438c3d99131b28a80d"; }, + @{EnvironmentRegex = "(AWS Production( Entrust)? 15)|(AWS Staging Lane MACUStg)"; APIKey = "fcb208ec98958282362c4e7879bfa38e4073c10c073cc2f"; LicenseKey = "77b3cae28080be6a5ebcb7d2f3efdde8d073cc2f"; }, + # Internal special-purpose accounts. + @{EnvironmentRegex = "^AWS Production 0$"; APIKey = "d34b1647dee7875568ebc4b7c97c37216c4c342b37bcce1"; LicenseKey = "b17e86388ca5c4a2c4777cd1b0e1e12a837bcce1"; } + ) + + $defaultAccountDetails = @( + # Legacy QA and Dev account. + @{EnvironmentRegex = "((? + + +Describe "Get-NewRelicAccountDetails" { + + Context "Correct NR Account Information is Returned for Various Environment Keys" { + + # Generic environment keys. + $defaultDrApiKey = "6a9e9820d3aaf12278c80d0bc1433b90aee1ea5769ac25f" + $defaultDrLicenseKey = "b55c8c163b1ede6fef75f6f586a1dbb6869ac25f" + + $defaultProdApiKey = "NRRA-8fbdf2188c09dd34aef478e13e31668e405324a304" + $defaultProdLicenseKey = "e0318c558f353e420720d4417ebe4cbd43b7d425" + + $defaultStagingApiKey = "a156af42c6da91536e67f2414bc1c25f3742e41b0cb93ab" + $defaultStagingLicenseKey = "3707290484dceca2dc2ecd71e40fb0ff40cb93ab" + + # FI environment keys. + $dsfcuApiKey = "b84b09aabf2ebe0773009698205220e9d53063bf84d041f" + $dsfcuLicenseKey = "417a21abe2bd84ca46940efc9c93cf75584d041f" + + $ftApiKey = "5362c58099d6a19871a07f66b1ec50e2c00f66b4b69260e" + $ftLicenseKey = "4e8134a277da93644407dc53931f81d99b69260e" + + $macuApiKey = "fcb208ec98958282362c4e7879bfa38e4073c10c073cc2f" + $macuLicenseKey = "77b3cae28080be6a5ebcb7d2f3efdde8d073cc2f" + + $otsApiKey = "3d0346ec856cc9b02b3b0fcc481afc989a4f9941b28a80d" + $otsLicenseKey = "d2eb268d151e57550de821438c3d99131b28a80d" + + $devApiKey = "0cdfe5a47b10db89acf3560e47d4657fbfec081040e50b8" + $devLicenseKey = "f88cf2f1ed5fca2600f6a49e63778274c40e50b8" + + $qaApiKey = "955b8bfccbdde190f855238dc55f17ba5d6382ee2492392" + $qaLicenseKey = "e941422aebc76c48d195bcdcf86e4f7602492392" + + #Internal environment keys. + $salesDemoApiKey = 'd34b1647dee7875568ebc4b7c97c37216c4c342b37bcce1' + $salesDemoLicenseKey = 'b17e86388ca5c4a2c4777cd1b0e1e12a837bcce1' + + # Test FirstTech + It "Returns Expected POD 5 Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production 5" + $curValues.APIKey | Should Be $ftApiKey + $curValues.LicenseKey | Should Be $ftLicenseKey + } + + It "Returns Expected Lane FTStg Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane FTStg" + $curValues.APIKey | Should Be $ftApiKey + $curValues.LicenseKey | Should Be $ftLicenseKey + } + + It "Returns Expected Lane FTDev Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane FTDev" + $curValues.APIKey | Should Be $ftApiKey + $curValues.LicenseKey | Should Be $ftLicenseKey + } + + It "Returns Expected Lane FTTest Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane FTTest" + $curValues.APIKey | Should Be $ftApiKey + $curValues.LicenseKey | Should Be $ftLicenseKey + } + + # Test DSFCU + It "Returns Expected POD 9 Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production 9" + $curValues.APIKey | Should Be $dsfcuApiKey + $curValues.LicenseKey | Should Be $dsfcuLicenseKey + } + + # Test OTS + It "Returns Expected POD 10 Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production 10" + $curValues.APIKey | Should Be $otsApiKey + $curValues.LicenseKey | Should Be $otsLicenseKey + } + + It "Returns Expected Lane OTSStg Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane OTSStg" + $curValues.APIKey | Should Be $otsApiKey + $curValues.LicenseKey | Should Be $otsLicenseKey + } + + # Test One of the DR PODs + It "Returns Expected DR Values" { + $curValues = Get-NewRelicAccountDetails "AWS DR Production 1" + $curValues.APIKey | Should Be $defaultDrApiKey + $curValues.LicenseKey | Should Be $defaultDrLicenseKey + } + + # Test One of the DR PODs With Their Own NR Account (should still use base DR key) + It "Returns Expected POD 9 DR Values" { + $curValues = Get-NewRelicAccountDetails "AWS DR Production 9" + $curValues.APIKey | Should Be $defaultDrApiKey + $curValues.LicenseKey | Should Be $defaultDrLicenseKey + } + + # Test MACU + It "Returns Expected Staging Lane MACUStg Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane MACUStg" + $curValues.APIKey | Should Be $macuApiKey + $curValues.LicenseKey | Should Be $macuLicenseKey + } + + It "Returns Expected AWS Production 15 Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production 15" + $curValues.APIKey | Should Be $macuApiKey + $curValues.LicenseKey | Should Be $macuLicenseKey + } + + It "Returns Expected AWS Production 15.0.1 Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production 15.0.1" + $curValues.APIKey | Should Be $macuApiKey + $curValues.LicenseKey | Should Be $macuLicenseKey + } + + # Test a Representative Set of "Default" Production Pods + $defaultPods = @( "AWS Production 1.1", "AWS Production 1", "AWS Production 2.2", "AWS Production 2.4", "AWS Production 6", "AWS Production 6.1", "AWS Production 7", "AWS Production 7.2", "AWS Production 8", "AWS Production 8.1", "AWS Production 12.2", "AWS Production 12.10", "AWS Production 14.3" ) + + It "Returns Expected Default Production Values" { + foreach ($pod in $defaultPods) { + $curValues = Get-NewRelicAccountDetails $pod + $curValues.APIKey | Should Be $defaultProdApiKey + $curValues.LicenseKey | Should Be $defaultProdLicenseKey + } + } + + # Test a representative set of "default" Staging Lanes + $defaultLanes = @("AWS Staging Lane LC1", "AWS Staging Lane PS5") + It "Returns Expected Default Staging Values" { + foreach ($lane in $defaultLanes) { + $curValues = Get-NewRelicAccountDetails $lane + $curValues.APIKey | Should Be $defaultStagingApiKey + $curValues.LicenseKey | Should Be $defaultStagingLicenseKey + } + } + + # Test that legacy Armor Staging lane names for First Tech no longer report the First Tech New Relic credentials. + It "Returns Expected Lane H (Deprecated) Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane H" + $curValues.APIKey | Should Be $defaultStagingApiKey + $curValues.LicenseKey | Should Be $defaultStagingLicenseKey + } + + It "Returns Expected FT Dev (Deprecated) Values" { + # This should throw because the name never conformed to conventions. + { Get-NewRelicAccountDetails "Staging First Tech Dev" } | Should -Throw + } + + It "Returns Expected FT Test (Deprecated) Values" { + # This should throw because the name never conformed to conventions. + { Get-NewRelicAccountDetails "Staging First Tech Test" } | Should -Throw + } + + # Test that legacy Armor Staging Lane name for MACU no longer reports the MACU New Relic credentials. + It "Returns Expected Staging Lane M (Deprecated) Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane M" + $curValues.APIKey | Should Be $defaultStagingApiKey + $curValues.LicenseKey | Should Be $defaultStagingLicenseKey + } + + # Test QA[x] matches the Dev New Relic key. + It "Returns Expected Dev Values for QA3" { + $curValues = Get-NewRelicAccountDetails "QA3" + $curValues.APIKey | Should Be $devApiKey + $curValues.LicenseKey | Should Be $devLicenseKey + } + + It "Returns Expected Dev Values for QA15" { + $curValues = Get-NewRelicAccountDetails "QW15" + $curValues.APIKey | Should Be $devApiKey + $curValues.LicenseKey | Should Be $devLicenseKey + } + + It "Returns Expected Dev Values for AWS DEV" { + $curValues = Get-NewRelicAccountDetails "AWS DEV TEAM RANDOM" + $curValues.APIKey | Should Be $devApiKey + $curValues.LicenseKey | Should Be $devLicenseKey + } + + It "Returns Expected QA Values for AWS QA" { + $curValues = Get-NewRelicAccountDetails "AWS QA REGRESSION BUGAPALOOZA" + $curValues.APIKey | Should Be $qaApiKey + $curValues.LicenseKey | Should Be $qaLicenseKey + } + + # Test QASTg does not match the Dev New Relic key. + It "Returns Expected Staging Values for QASTg" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Lane QASTG" + $curValues.APIKey | Should Be $defaultStagingApiKey + $curValues.LicenseKey | Should Be $defaultStagingLicenseKey + } + + It "Returns Expected Values for Pod 0 (Sales Demo)" { + $curValues = Get-NewRelicAccountDetails "AWS Production 0" + $curValues.APIKey | Should Be $salesDemoApiKey + $curValues.LicenseKey | Should Be $salesDemoLicenseKey + } + + It "Returns Expected Values for Pod 0.1 (Pen Test)" { + $curValues = Get-NewRelicAccountDetails "AWS Production 0.1" + $curValues.APIKey | Should Be $defaultProdApiKey + $curValues.LicenseKey | Should Be $defaultProdLicenseKey + } + + # Test Entrust environment keys. + It "Returns Expected First Tech Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production Entrust 5-0" + $curValues.APIKey | Should Be $ftApiKey + $curValues.LicenseKey | Should Be $ftLicenseKey + } + + It "Returns Expected Desert Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production Entrust 9-0" + $curValues.APIKey | Should Be $dsfcuApiKey + $curValues.LicenseKey | Should Be $dsfcuLicenseKey + } + + It "Returns Expected OTS Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production Entrust 10-0" + $curValues.APIKey | Should Be $otsApiKey + $curValues.LicenseKey | Should Be $otsLicenseKey + } + + It "Returns Expected MACU Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production Entrust 15-0" + $curValues.APIKey | Should Be $macuApiKey + $curValues.LicenseKey | Should Be $macuLicenseKey + } + + It "Returns Expected Staging Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Staging Entrust" + $curValues.APIKey | Should Be $defaultStagingApiKey + $curValues.LicenseKey | Should Be $defaultStagingLicenseKey + } + + It "Returns Expected Default Production Entrust Values" { + $curValues = Get-NewRelicAccountDetails "AWS Production Entrust 12-0" + $curValues.APIKey | Should Be $defaultProdApiKey + $curValues.LicenseKey | Should Be $defaultProdLicenseKey + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.ps1 new file mode 100644 index 0000000..f5be874 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.ps1 @@ -0,0 +1,25 @@ +function Get-NewRelicYamlPath { + <# + .SYNOPSIS + Get New Relic Infrastructure yaml file location + + .DESCRIPTION + Fetch a hard-coded yaml file location for the New Relic Infrastructure + + .EXAMPLE + Get-NewRelicYamlPath + #> + + [CmdletBinding()] + param() + $logLead = Get-LogLeadName + $newRelicYaml = "C:\Program Files\New Relic\newrelic-infra\newrelic-infra.yml" + + if (!(Test-Path -Path $newRelicYaml)) { + Write-Warning "$loglead : File does not exist: $newRelicYaml" + return + } + + return $newRelicYaml + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.tests.ps1 new file mode 100644 index 0000000..45d2642 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-NewRelicYamlPath.tests.ps1 @@ -0,0 +1,37 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NewRelicYamlPath" { + Mock -CommandName Get-LogLeadName -MockWith {} + Mock -CommandName Test-Path -MockWith { $true } + + Context "Validation" { + + It "Finds the yaml" { + + Mock -CommandName Write-Warning -MockWith {} + Get-NewRelicYamlPath | Should -Be "C:\Program Files\New Relic\newrelic-infra\newrelic-infra.yml" + } + + It "When a yaml file is not found, It writes a warning" { + + Mock -CommandName Test-Path -MockWith { $false } + + Get-NewRelicYamlPath + Assert-MockCalled -CommandName Write-Warning -Scope Context -ParameterFilter { $Message -like "*File does not exist:*" } + } + + It "Returns null" { + + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Test-Path -MockWith { $false } + + Get-NewRelicYamlPath | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.Tests.ps1 new file mode 100644 index 0000000..8c3f1e8 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.Tests.ps1 @@ -0,0 +1,208 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe 'Get_PackageServerMappings'{ + Mock -CommandName Write-Host -MockWith {} + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Write-Error -MockWith {} + Mock -CommandName Get-MicroserviceNewRelicMapping -MockWith { + param( + $BuildCheckoutDirectory, + [PSObject]$PackageMetadata) + return $PackageMetadata + } + Mock -CommandName ConvertTo-SafeTeamCityMessage -MockWith {} + + Context "When Processing Packages" { + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + $appPackage3 = New-DummyPackageInstallationData -PackageName "invalid.app.package1" -PackageVersion "1.0.0" -IsFullScale -IsValid $false + $appPackage4 = New-DummyPackageInstallationData -PackageName "mic.package1" -PackageVersion "1.0.0" -IsFullScale + $webPackage1 = New-DummyPackageInstallationData -PackageName "web.package1" -PackageVersion "1.0.0" -IsFullScale + $webPackage2 = New-DummyPackageInstallationData -PackageName "web.package2" -PackageVersion "1.0.0" -IsFullScale + $webPackage3 = New-DummyPackageInstallationData -PackageName "invalid.web.package1" -PackageVersion "1.0.0" -IsFullScale -IsValid $false + + $appPackageArray = @($appPackage1, $appPackage2, $appPackage4) + $webPackageArray = @($webPackage1, $webPackage2) + + $appPackage3.IsValid = $False + $webPackage3.IsValid = $False + + $invalidAppPackageArray = @($appPackage1, $appPackage2, $appPackage3, $appPackage4) + $invalidWebPackageArray = @($webPackage1, $webPackage2, $webPackage3) + + # Send back app/mic packages if given app packages + Mock -CommandName Get-PackageInstallationData -MockWith { + # only the packages matter here, but we get all of them, so accept them in case they're useful for a future mock. + param( + $ChocoPackages, + $Servers, + $FilterFeeds, + $FilterPowerShellModules, + $NugetCredential, + $IncludeMissingPackages, + $UseV2PackageMetadata + ) + + $packages = @() + foreach ($package in $ChocoPackages){ + if($package.Name -like "*app*"){ + $newPackage = New-DummyPackageInstallationData -PackageName $package.Name -PackageVersion $package.Version -IsAppOnly -IsValid $package.IsValid + } else { + $newPackage = New-DummyPackageInstallationData -PackageName $package.Name -PackageVersion $package.Version -IsMicOnly -IsValid $package.IsValid + } + + $packages += $newPackage + } + + return $packages + } -ParameterFilter {($ChocoPackages[0].Name -like "*app*" -or $ChocoPackages[0].Name -like "*mic*")} + + # Send back app/mic packages if given app packages + Mock -CommandName Get-PackageInstallationData -MockWith { + # only the packages matter here, but we get all of them, so accept them in case they're useful for a future mock. + param( + $ChocoPackages, + $Servers, + $FilterFeeds, + $FilterPowerShellModules, + $NugetCredential, + $IncludeMissingPackages, + $UseV2PackageMetadata + ) + + $packages = @() + foreach ($package in $ChocoPackages){ + $newPackage = New-DummyPackageInstallationData -PackageName $package.Name -PackageVersion $package.Version -IsWebOnly -IsValid $package.IsValid + $packages += $newPackage + } + + return $packages + } -ParameterFilter {$ChocoPackages[0].Name -like "*web*"} + + $packageData = New-PackageMetadataObject + $packageData.DisableMicroserviceNewRelic = $true + $packageData.Servers = @("fakeServer.1","fakeServer.2") + + $password = ConvertTo-SecureString 'fake' -AsPlainText -Force + $fakeCred = New-Object System.Management.Automation.PSCredential('bogus', $password) + + $fakeDebugMetadata = New-Object psobject -property @{ + WebServerPackages = @($webPackage1,$webPackage2) + AppServerPackages = @($appPackage1, $appPackage2) + MicServerPackages = @($appPackage1, $appPackage2) + FabServerPackages = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + } + + It "Populates WebPackagesToInstall" { + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.WebPackagesToInstall | Should -Not -BeNullOrEmpty + } + + It "Populates AppPackagesToInstall" { + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.AppPackagesToInstall | Should -Not -BeNullOrEmpty + } + + It "Populates DebugMetadata ClassifiedPackagesMap" { + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $debugMetadata.ClassifiedPackagesMap | Should -Not -BeNullOrEmpty + } + + It "Sets Microservice New Relic Settings" { + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + Assert-MockCalled -CommandName Get-MicroserviceNewRelicMapping -Scope Context -Times 1 + } + + It "Writes Warnings when given invalid App packages"{ + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $invalidAppPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + Assert-MockCalled -CommandName Write-Warning -Scope Context -ParameterFilter {$Message -like "*MISSING_PACKAGES*"} + Assert-MockCalled -CommandName Write-Warning -Scope Context -ParameterFilter {$Message -like "*MISSING_APP_PACKAGE*"} + } + + It "Writes Warnings when given invalid Web packages"{ + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $invalidWebPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + Assert-MockCalled -CommandName Write-Warning -Scope Context -ParameterFilter {$Message -like "*MISSING_PACKAGES*"} + Assert-MockCalled -CommandName Write-Warning -Scope Context -ParameterFilter {$Message -like "*MISSING_WEB_PACKAGE*"} + } + + It "Sets Install To App With App Servers" { + $packageData.InstallToAppsAndMics = $true + $packageData.HasAppServers = $true + + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.AppPackagesToInstall | Should -Not -BeNullOrEmpty + + ($packageData.AppPackagesToInstall.InstallToApp -contains $true) | Should -BeTrue + } + + It "Doesn't Set Install To App Without App Servers" { + $packageData.InstallToAppsAndMics = $true + $packageData.HasAppServers = $false + + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.AppPackagesToInstall | Should -Not -BeNullOrEmpty + + ($packageData.AppPackagesToInstall.InstallToApp -contains $true) | Should -BeFalse + } + + It "Sets Install To Mic With Mic Servers" { + $packageData.InstallToAppsAndMics = $true + $packageData.HasMicServers = $true + + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.AppPackagesToInstall | Should -Not -BeNullOrEmpty + ($packageData.AppPackagesToInstall.InstallToMic -contains $true) | Should -BeTrue + } + + It "Doesn't Install To Mic Without Mic Servers" { + $packageData.InstallToAppsAndMics = $true + $packageData.HasMicServers = $false + + $packageMetadata,$debugMetadata = Get-PackageServerMapping -AllAppTierPackages $appPackageArray -AllWebTierPackages $webPackageArray ` + -BuildCheckoutDirectory "fakeDirectory" -DebugMetadata $fakeDebugMetadata ` + -NugetCredential $fakeCred -PackageMetadata $packageData + + $packageData.AppPackagesToInstall | Should -Not -BeNullOrEmpty + ($packageData.AppPackagesToInstall.InstallToMic -contains $true) | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.ps1 new file mode 100644 index 0000000..14df3ec --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-PackageServerMapping.ps1 @@ -0,0 +1,176 @@ +function Get-PackageServerMapping { + <# + .SYNOPSIS + The bulk of the Classify logic. Determines what packages go where. Utilized by Classify-Packages. + + .PARAMETER AllAppTierPackages + App tier packages to install. + + .PARAMETER AllWebTierPackages + Web tier packages to install. + + .PARAMETER BuildCheckoutDirectory + Directory where the code is checked out by TC. + + .PARAMETER DebugMetadata + Meta object used to determine what goes where. + + .PARAMETER NugetCredential, + Credentials to access nuget/proget. + + .PARAMETER PackageMetadata + Packages object to populate. + + .PARAMETER UseV2PackageMetadata + Whether or not to use the new version of the PackageMetadata functions. + + .PARAMETER DependencyReleaseValue + Used for determining what version of orb we're installing, if it's an orb deploy. + #> + [CmdletBinding()] + param( + $AllAppTierPackages, + $AllWebTierPackages, + $BuildCheckoutDirectory, + $DebugMetadata, + [System.Management.Automation.PSCredential]$NugetCredential, + [PSObject]$PackageMetadata, + [switch]$UseV2PackageMetadata, + [string]$DependencyReleaseValue = "" + ) + + $loglead = Get-LogLeadName + + # Force the results of these two calls into arrays. Necessary for testing, and it's what we should be getting anyways. + # PS tries to unbox single items. No bueno. + Write-Host "$loglead Classifying Web Packages Being Installed" + $PackageMetadata.WebPackagesToInstall = @(Get-PackageInstallationData -ChocoPackages $AllWebTierPackages -Servers $PackageMetadata.Servers -FilterFeeds -FilterPowerShellModules -NugetCredential $NugetCredential -IncludeMissingPackages -UseV2PackageMetadata:$UseV2PackageMetadata -PackageToServerMap $PackageMetadata.PackageToServers -ErrorAction Continue) + Write-Host "$loglead Classifying App Packages Being Installed" + $PackageMetadata.AppPackagesToInstall = @(Get-PackageInstallationData -ChocoPackages $AllAppTierPackages -Servers $PackageMetadata.Servers -FilterFeeds -FilterPowerShellModules -NugetCredential $NugetCredential -IncludeMissingPackages -UseV2PackageMetadata:$UseV2PackageMetadata -PackageToServerMap $PackageMetadata.PackageToServers -ErrorAction Continue) + + # The above forced typecasting into an array broke a bunch of stuff because it forced the values to be @($null) rather than just $null + # This puts it back the way it was, but we're not removing any of the down the line null checks. + if($PackageMetadata.WebPackagesToInstall.Count -eq 1 -and $null -eq $PackageMetadata.WebPackagesToInstall[0]) + { + $PackageMetadata.WebPackagesToInstall = @() + } + if($PackageMetadata.AppPackagesToInstall.Count -eq 1 -and $null -eq $PackageMetadata.AppPackagesToInstall[0]) + { + $PackageMetadata.AppPackagesToInstall = @() + } + + $PackageMetadata = Remove-OutdatedNewHotfixPackages -PackageMetadata $PackageMetadata -DependencyReleaseValue $DependencyReleaseValue + + # Set which microservices should have New Relic enabled/disabled. + $PackageMetadata = Get-MicroserviceNewRelicMapping -BuildCheckoutDirectory $BuildCheckoutDirectory -PackageMetadata $PackageMetadata + + # Notify TC of missing packages. + if ($PackageMetadata.WebPackagesToInstall.Where({ $_.IsValid -eq $false}).Count -gt 0 -or $PackageMetadata.AppPackagesToInstall.Where({ $_.IsValid -eq $false}).Count -gt 0) { + $message = "Packages currently deployed to the system cannot be located in any configured feed. Were they renamed or removed from the feed?" + Write-Warning "$loglead MISSING_PACKAGES : $message" + Write-Warning "$loglead ORB Platform deploys reinstall Widgets and Providers that are currently installed." + Write-Host "##teamcity[buildProblem description='$message' identity='MISSING_PACKAGES']" + } + + if ($PackageMetadata.WebPackagesToInstall.Where({ $_.IsValid -eq $false}).Count -gt 0) { + $missingWebPackages = $PackageMetadata.WebPackagesToInstall | Where-Object { $_.IsValid -eq $false } + foreach ($missingWebPackage in $missingWebPackages) { + $missingWebPackageDetail = "$($missingWebPackage.Name)|$($missingWebPackage.Version)" + $tcMissingWebPackageDetail = ConvertTo-SafeTeamCityMessage -InputText $missingWebPackageDetail + $missingWebPackageName = $missingWebPackage.Name + Write-Warning "$loglead MISSING_WEB_PACKAGE : $missingWebPackageDetail" + Write-Host "##teamcity[buildProblem description='Missing web package: $tcMissingWebPackageDetail' identity='MISSINGWEB_$missingWebPackageName']" + } + } + + if ($PackageMetadata.AppPackagesToInstall.Where({ $_.IsValid -eq $false}).Count -gt 0) { + $missingAppPackages = $PackageMetadata.AppPackagesToInstall | Where-Object { $_.IsValid -eq $false } + foreach ($missingAppPackage in $missingAppPackages) { + $missingAppPackageDetail = "$($missingAppPackage.Name)|$($missingAppPackage.Version)" + $tcMissingAppPackageDetail = ConvertTo-SafeTeamCityMessage -InputText $missingAppPackageDetail + $missingAppPackageName = $missingAppPackage.Name + Write-Warning "$loglead MISSING_APP_PACKAGE : $missingAppPackageDetail" + Write-Host "##teamcity[buildProblem description='Missing app package: $tcMissingAppPackageDetail' identity='MISSINGAPP_$missingAppPackageName']" + } + } + + # Determine if app packages are going to app servers, mic servers, or both. + if ($PackageMetadata.InstallToAppsAndMics) { + foreach ($package in $PackageMetadata.AppPackagesToInstall) { + $install = $package.InstallToApp -or $package.InstallToMic + $package.InstallToApp = $PackageMetadata.HasAppServers -and $install + $package.InstallToMic = $PackageMetadata.HasMicServers -and $install + } + } + + # Create a map of classified packages, so we can more easily find name->classifiedPackage + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.WebPackagesToInstall)) { + Write-Host "$loglead Creating Web Package Map:" + foreach ($package in $PackageMetadata.WebPackagesToInstall) { + if([string]::IsNullOrWhiteSpace($package.Name)){ + Write-Warning "$loglead Got an empty Web package name: $package!" + } else { + $DebugMetadata.ClassifiedPackagesMap[$package.Name.ToLower()] = $package; + } + } + } + + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.AppPackagesToInstall)) { + Write-Host "$loglead Creating App Package Map:" + foreach ($package in $PackageMetadata.AppPackagesToInstall) { + if([string]::IsNullOrWhiteSpace($package.Name)){ + Write-Warning "$loglead Got an empty App package name: $package!" + } else { + $DebugMetadata.ClassifiedPackagesMap[$package.Name.ToLower()] = $package; + } + } + } + + #region Get MigrationOnly Packages + # It is ok, per Cole, that migration packages can be specified in either the web or app install boxes in the UI + # We do not care that they show up in the uninstall boxes, yet (future work SRE-16946) + $webIsMigrationPackage = $PackageMetadata.WebPackagesToInstall.Where({$_.IsMigrationPackage -eq $true}) + $appIsMigrationPackage = $PackageMetadata.AppPackagesToInstall.Where({$_.IsMigrationPackage -eq $true}) + $migrationOnlyPackages = @() + if (-not (Test-IsCollectionNullOrEmpty -Collection $webIsMigrationPackage)) { + foreach ($migrationPackage in $webIsMigrationPackage) { + Write-Host "$logLead : Found a migration package in the web inputs: $($migrationPackage.Name)" + + $DebugMetadata.IsMigrationPackage.Web += $migrationPackage + + if ($migrationPackage.Name -notin $migrationOnlyPackages.Name) { + $migrationOnlyPackages += $migrationPackage + } else { + # TODO - This is not aware of versions, this may bite us later, not worth the effort to pre-worry about that today + # The circumstances of them putting two different migration versions in the install lists for the same package name is so low, we should just tell them to do better + # There is a point of diminishing returns on holding their hands - cbrand 2022-07-12 + Write-Host "$logLead : Migration package with Name $($migrationPackage.Name) already in `$migrationOnlyPackages collection, not adding a duplicate-by-name" + } + } + } else { + Write-Host "$logLead : Did not find any migration packages to be installed in the web inputs" + } + if (-not (Test-IsCollectionNullOrEmpty -Collection $appIsMigrationPackage)) { + foreach ($migrationPackage in $appIsMigrationPackage) { + Write-Host "$logLead : Found a migration package in the app inputs: $($migrationPackage.Name)" + + $DebugMetadata.IsMigrationPackage.App += $migrationPackage + + if ($migrationPackage.Name -notin $migrationOnlyPackages.Name) { + $migrationOnlyPackages += $migrationPackage + } else { + # TODO - This is not aware of versions, this may bite us later, not worth the effort to pre-worry about that today + # The circumstances of them putting two different migration versions in the install lists for the same package name is so low, we should just tell them to do better + # There is a point of diminishing returns on holding their hands - cbrand 2022-07-12 + Write-Host "$logLead : Migration package with Name $($migrationPackage.Name) already in `$migrationOnlyPackages collection, not adding a duplicate-by-name" + } + } + } else { + Write-Host "$logLead : Did not find any migration packages to be installed in the app inputs" + } + + $PackageMetadata.MigrationOnlyPackages = $migrationOnlyPackages + #endregion Get MigrationOnly Packages + + return $PackageMetadata, $DebugMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.Tests.ps1 new file mode 100644 index 0000000..1beb6cf --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.Tests.ps1 @@ -0,0 +1,620 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +# Init functions. Used to reset package objets in each Context. +function Init-PackageData { + $webPackage1 = New-DummyPackageInstallationData -PackageName "web.package1" -PackageVersion "1.1.0" -IsWebOnly + $webPackage2 = New-DummyPackageInstallationData -PackageName "web.package2" -PackageVersion "1.1.0" -IsWebOnly + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.1.0" -IsAppOnly + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.1.0" -IsAppOnly + $micPackage1 = New-DummyPackageInstallationData -PackageName "mic.package1" -PackageVersion "1.1.0" -IsMicOnly + $micPackage2 = New-DummyPackageInstallationData -PackageName "mic.package2" -PackageVersion "1.1.0" -IsMicOnly + + $appPackageArray = @($appPackage1, $appPackage2) + $webPackageArray = @($webPackage1, $webPackage2) + $micPackageArray = @($micPackage1, $micPackage2) + + $packageData = New-PackageMetadataObject + + $packageData.AppPackagesToInstall = $appPackageArray + $packageData.AppPackagesToInstall += $micPackageArray + $packageData.WebPackagesToInstall = $webPackageArray + $packageData.ServersToQuery = @("fake.appserver.1", "fake.fabserver.1", "fake.micserver.1", "fake.webserver.1") + $packageData.AppServers = @("fake.appserver.1") + $packageData.FabServers = @("fake.fabserver.1") + $packageData.MicServers = @("fake.micserver.1") + $packageData.WebServers = @("fake.webserver.1") + $packageData.HasAppServers = $true + $packageData.HasFabServers = $true + $packageData.HasMicServers = $true + $packageData.HasWebServers = $true + + return $packageData +} + +function Init-DebugMetadata { + $webPackage1 = New-DummyPackageInstallationData -PackageName "web.package1" -PackageVersion "1.1.0" -IsWebOnly + $webPackage2 = New-DummyPackageInstallationData -PackageName "web.package2" -PackageVersion "1.1.0" -IsWebOnly + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.1.0" -IsAppOnly + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.1.0" -IsAppOnly + $micPackage1 = New-DummyPackageInstallationData -PackageName "mic.package1" -PackageVersion "1.1.0" -IsMicOnly + $micPackage2 = New-DummyPackageInstallationData -PackageName "mic.package2" -PackageVersion "1.1.0" -IsMicOnly + + $debugMetadata = New-Object psobject -property @{ + WebServerPackages = @() + AppServerPackages = @() + MicServerPackages = @() + FabServerPackages = @() + + WebPackagesToInstallMap = @{} + AppPackagesToInstallMap = @{} + ClassifiedPackagesMap = @{} + ExistingInstalledHotfixes = @() + } + + $appServerPackageArray = @($appPackage1, $appPackage2, $webPackage1) + $webServerPackageArray = @($webPackage1, $webPackage2, $appPackage1) + $micServerPackageArray = @($micPackage1, $micPackage2, $webPackage1) + $fabServerPackageArray = @($micPackage1, $micPackage2, $webPackage1) + + $debugMetadata.WebServerPackages = $webServerPackageArray + $debugMetadata.AppServerPackages = $appServerPackageArray + $debugMetadata.MicServerPackages = $micServerPackageArray + $debugMetadata.FabServerPackages = $fabServerPackageArray + + $debugMetadata.ClassifiedPackagesMap["app.package1"] = $appPackage1 + $debugMetadata.ClassifiedPackagesMap["app.package2"] = $appPackage2 + $debugMetadata.ClassifiedPackagesMap["mic.package1"] = $micPackage1 + $debugMetadata.ClassifiedPackagesMap["mic.package2"] = $micPackage2 + $debugMetadata.ClassifiedPackagesMap["web.package1"] = $webPackage1 + $debugMetadata.ClassifiedPackagesMap["web.package2"] = $webPackage2 + + return $debugMetadata +} + +$password = ConvertTo-SecureString 'fake' -AsPlainText -Force +$fakeCred = New-Object System.Management.Automation.PSCredential('bogus', $password) + +Describe "Get-ServerPackageInformation" { + #region Mocks + Mock -CommandName Write-Host -MockWith {} + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Write-Error -MockWith {} + + #TODO : Check the return value; possibly needs to be massaged. If so, might need a duplicate in a specific Context. + Mock -CommandName Get-EnvironmentData -MockWith { return $packageData } + + #TODO : Check the return value; possibly needs to be massaged. If so, might need a duplicate in a specific Context. + Mock -CommandName Get-PackageServerMapping -MockWith { return $packageData, $debugMetadata } + + #TODO : Check the return value; possibly needs to be massaged. If so, might need a duplicate in a specific Context. + Mock -CommandName Get-BadPackages -MockWith { return $packageData } + + Mock -CommandName Select-InstallPackages -MockWith { return $packageData } + + Mock -CommandName Get-AwsSettings -MockWith { + return @{ + "Region" = "courts-of-chaos-1" + "Profile" = "unittest" + } + } + + # Cheating for the mock; assume you always get a unique list and just return it. + # Only works if you ensure the input is faked correctly. + Mock -CommandName Select-UniqueServerPackages -MockWith { return $packagesArray } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.0" + Name = "web.package1" + IsValid = $false + }, + @{ + Version = "1.0.0" + Name = "web.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*web*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.0" + Name = "mic.package1" + IsValid = $false + }, + @{ + Version = "1.0.0" + Name = "mic.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*mic*" -or $ComputerName -like "*fab*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.0" + Name = "app.package1" + IsValid = $false + }, + @{ + Version = "1.0.0" + Name = "app.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*app*" } + + # TODO : Check the return value; Possibly needs to be massaged. + # Cheating for the mock; assume you always get a unique list and just return it. + # Only works if you ensure the input is faked correctly. + Mock -CommandName Remove-PackagesThatAreAlreadyInstalled -MockWith { + return $packagesToInstall + } + + Mock -CommandName Select-AlkamiAppServers -MockWith {return $true} + Mock -CommandName Select-AlkamiMicServers -MockWith {return $true} + Mock -CommandName Select-AlkamiWebServers -MockWith {return $true} + + #endregion Mocks + + Context "Checking that Dependencies are used" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + It "Gets EnvironmentData" { + Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + Assert-MockCalled -CommandName Get-EnvironmentData + } + It "Builds PackageServerMapping" { + Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + Assert-MockCalled -CommandName Get-PackageServerMapping + } + It "Gets EnvironmentData" { + Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + Assert-MockCalled -CommandName Get-BadPackages + } + It "Gets EnvironmentData" { + Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + Assert-MockCalled -CommandName Get-AwsSettings + } + } + + # We force reinstall Widgets & Providers if it's an orb deploy. We don't if it's a package install. + Context "When Force Re-installing Packages" { + + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + # Create Pre-installed packages + $appReInstallPackage = New-DummyPackageInstallationData -packagename "app.reinstall.package" -packageversion "1.0.0" -isapponly + $webReInstallPackage = New-DummyPackageInstallationData -packagename "web.reinstall.package" -packageversion "1.0.0" -iswebonly + + # Create Packages to be uninstalled + $appUninstallPackage = New-DummyPackageInstallationData -packagename "app.uninstall.package" -packageversion "1.0.0" -isapponly + $webuninstallPackage = New-DummyPackageInstallationData -packagename "web.uninstall.package" -packageversion "1.0.0" -iswebonly + + # Create pre installed Packages to be uninstalled and reinstalled. + $appReUninstallPackage = New-DummyPackageInstallationData -packagename "app.re.uninstall.package" -packageversion "1.0.0" -isapponly + $webReUninstallPackage = New-DummyPackageInstallationData -packagename "web.re.uninstall.package" -packageversion "1.0.0" -iswebonly + + # Add packages to install/uninstall lists + $packagedata.AppPackagesToUninstall += @($appReUninstallPackage) + $packagedata.WebPackagesToUninstall += @($webReUninstallPackage) + + $packagedata.AppPackagesToUninstall += @($appUninstallPackage) + $packagedata.WebPackagesToUninstall += @($webUninstallPackage) + + $packageData.AppPackagesToInstall += @($appReUninstallPackage) + $packageData.WebPackagesToInstall += @($webReUninstallPackage) + + # Make this an orb run + $packageData.ForceReinstallPackages = $true + + # Add packages to preinstalled lists + $debugMetadata.AppServerPackages += $appReInstallPackage + $debugMetadata.WebServerPackages += $webReInstallPackage + + $debugMetadata.AppServerPackages += $appUninstallPackage + $debugMetadata.WebServerPackages += $webUninstallPackage + + $debugMetadata.AppServerPackages += $appReUninstallPackage + $debugMetadata.WebServerPackages += $webReUninstallPackage + + # Pretend packages to be reinstalled are everywhere + Mock -CommandName Select-InstallPackages -MockWith { + $appReInstallPackage.ForceSameVersion = $true + $webReInstallPackage.ForceSameVersion = $true + + $newPackages = @() + $newPackages += $appReInstallPackage + $newPackages += $webReInstallPackage + + # TODO: Change this to be uninstall/reinstall packages. + $newPackages += $appReUninstallPackage + $newPackages += $webReUninstallPackage + + $newPackages += $packages + + return $newPackages + } + + Mock -CommandName Get-PackageServerMapping -MockWith{ + $PackageMetadata.AppPackagesToInstall = $AllAppTierPackages + $PackageMetadata.WebPackagesToInstall = $AllWebTierPackages + + return $PackageMetadata, $DebugMetadata + } + + Mock -CommandName Add-OldHotfixPackagesToUninstallList -MockWith { + return $PackageMetadata, $DebugMetadata + } + + $packages, $debugpackagses = Get-ServerPackageInformation -packagemetadata $packageData -debugmetadata $debugmetadata ` + -buildcheckoutdirectory "somepath" -nugetcredential $fakecred -DependencyReleaseValue "123" -AwsProfileName "JunkProfile" + + It "Calls Add-OldHotfixPackagesToUninstallList" { + Assert-MockCalled Add-OldHotfixPackagesToUninstallList + } + + # Test App Packages + It "Keeps Providers To Be Reinstalled"{ + $packages.AppPackagesToInstall | Should -Contain $appReInstallPackage + } + + # Test Web Packages + It "Keeps Widgets To Be Reinstalled" { + $packages.WebPackagesToInstall | Should -Contain $WebReInstallPackage + } + + It "Does Not Reinstall Packages Which Are Being Uninstalled" { + $packages.AppPackagesToInstall | Should -Not -Contain $appUninstallPackage + $packages.WebPackagesToInstall | Should -Not -Contain $webUninstallPackage + } + + It "Does Reinstall Packages Which Are Being Uninstalled And Reinstalled" { + $packages.AppPackagesToInstall | Should -Contain $appReUninstallPackage + $packages.WebPackagesToInstall | Should -Contain $webReUninstallPackage + } + } + + Context "When Told To Install Chocolatey" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + $packageData.AppPackagesToInstall += New-DummyPackageInstallationData -PackageName "chocolatey" -PackageVersion "1.0.0" -IsAppOnly + $packageData.WebPackagesToInstall += New-DummyPackageInstallationData -PackageName "chocolatey" -PackageVersion "1.0.0" -IsWebOnly + + # This is the function that strips out chocolatey based on allWebTierPackages and allAppTierPackages. Do that here. + Mock -CommandName Get-PackageServerMapping -MockWith { + $packageData.AppPackagesToInstall = $AllAppTierPackages + $packageData.WebPackagesToInstall = $AllWebTierPackages + + return $packageData, $debugMetadata } + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + It "Writes a Warning on Apps" { + Assert-MockCalled -CommandName Write-Warning -Scope Context -Exactly 1 ` + -ParameterFilter {$Message -eq "The Chocolatey Package Manager Program package (chocolatey) was passed in to be installed on the App Tier. At this time we explicitly dissallow that. It has been skipped."} + } + It "Writes a Warning on Webs" { + Assert-MockCalled -CommandName Write-Warning -Scope Context -Exactly 1 ` + -ParameterFilter {$Message -eq "The Chocolatey Package Manager Program package (chocolatey) was passed in to be installed on the Web Tier. At this time we explicitly dissallow that. It has been skipped."} + } + It "Removes Chocolatey From the Install List on Apps" { + $packages.AppPackagesToInstall | ForEach-Object {$_.Name | Should -Not -BeExactly "chocolatey"} + } + It "Removes Chocolatey from The Install List on Webs" { + $packages.WebPackagesToInstall | ForEach-Object {$_.Name | Should -Not -BeExactly "chocolatey"} + } + } + + # TODO: Need changes from SRE-16772 before we write tests + Context "When Different Servers Have Different Package Versions" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + $packageData.ServersToQuery = @("fake.appserver.1", "fake.appserver.2", "fake.fabserver.1", "fake.fabserver.2", "fake.micserver.1", "fake.micserver.2", "fake.webserver.1", "fake.webserver.2") + + # Need one mock per server based on computer name. So, 8 mocks. + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "web.package1" + IsValid = $false + }, + @{ + Version = "1.0.1" + Name = "web.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.webserver.1*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "web.package1" + IsValid = $false + }, + @{ + Version = "1.0.2" + Name = "web.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.webserver.2*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "mic.package1" + IsValid = $false + }, + @{ + Version = "1.0.1" + Name = "mic.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.micserver.1*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "mic.package1" + IsValid = $false + }, + @{ + Version = "1.0.2" + Name = "mic.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.micserver.2*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "mic.package1" + IsValid = $false + }, + @{ + Version = "1.0.1" + Name = "mic.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.fabserver.1*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "mic.package1" + IsValid = $false + }, + @{ + Version = "1.0.2" + Name = "mic.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.fabserver.2*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "app.package1" + IsValid = $false + }, + @{ + Version = "1.0.1" + Name = "app.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.appserver.1*" } + + Mock -CommandName Get-InstalledPackages -MockWith { $installedPackages = @( @{ + Version = "1.0.1" + Name = "app.package1" + IsValid = $false + }, + @{ + Version = "1.0.2" + Name = "app.package2" + IsValid = $false + } + ) + + return [pscustomobject]$installedPackages + } -ParameterFilter { $ComputerName -like "*fake.appserver.2*" } + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + it "Reduces Them to Unique Values" { + $packages.PackageToVersions | should -not -BeNullOrEmpty + foreach ($h in $packages.packagetoversions.getenumerator()) { + $versions = $h.value + foreach ($version in $versions) { + ($versions | Where-Object { $_ -eq $version }).Count | Should Be 1 + } + } + } + } + + Context "When Already Installed Packages Are To Be Re-installed" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + # Create pre-installed package + $appInstalledPackage = New-DummyPackageInstallationData -packagename "app.package1" -packageversion "1.0.0" -isapponly + + # Create package to install + $appPackageV2 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "2.1.0" -IsAppOnly + + # Add package to preinstalled list + $debugMetadata.AppServerPackages += $appInstalledPackage + + # Add package to list to be installed + $packageData.AppPackagesToInstall += @($appPackageV2) + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + It "Calls Remove-PackagesThatAreAlreadyInstalled" { + + Assert-MockCalled -CommandName Remove-PackagesThatAreAlreadyInstalled -Scope Context + } + + It "Installs The Version From The Install List" { + $packages.AppPackagesToInstall | Should -Contain $appPackageV2 + } + + It "Does Not Install The Existing Version" { + $packages.AppPackagesToInstall | Should -Not -Contain $appInstalledPackage + } + } + + Context "When Uninstalling Packages" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + $appUninstallPackage = New-DummyPackageInstallationData -PackageName "app.uninstall.package" -PackageVersion "1.0.0" -IsAppOnly + $micUninstallPackage = New-DummyPackageInstallationData -PackageName "mic.uninstall.package" -PackageVersion "1.0.0" -IsMicOnly + $webUninstallPackage = New-DummyPackageInstallationData -PackageName "web.uninstall.package" -PackageVersion "1.0.0" -IsWebOnly + + # Starts off in the same array, split inside the function we're testing. + $packageData.AppPackagesToUninstall += @($appUninstallPackage) + $packageData.AppPackagesToUninstall += @($micUninstallPackage) + + $packageData.WebPackagesToUninstall += @($webUninstallPackage) + + $debugMetadata.AppServerPackages += $appUninstallPackage + $debugMetadata.MicServerPackages += $micUninstallPackage + $debugMetadata.WebServerPackages += $webUninstallPackage + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + It "Removes Packages from App Servers" { + $packages.AppPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.AppPackagesToUninstall | Should -Match $packageData.AppPackagesToUninstall + } + + It "Removes Packages from Mic Servers" { + $packages.MicPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.MicPackagesToUninstall | Should -Match $packageData.MicPackagesToUninstall + } + + It "Removes Packages from Web Servers" { + $packages.WebPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.WebPackagesToUninstall | Should -Match $packageData.WebPackagesToUninstall + } + + It "Correctly Labels Has App Uninstalls" { + $packages.HasAppUninstalls | Should -BeTrue + } + It "Correctly Labels Has Mic Uninstalls" { + $packages.HasMicUninstalls | Should -BeTrue + } + It "Correctly Labels Has Web Uninstalls" { + $packages.HasWebUninstalls | Should -BeTrue + } + } + + Context "When Packages Are Found On The Wrong Server Type" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + $appUninstallPackage = New-DummyPackageInstallationData -PackageName "app.uninstall.package" -PackageVersion "1.0.0" -IsAppOnly + $micUninstallPackage = New-DummyPackageInstallationData -PackageName "mic.uninstall.package" -PackageVersion "1.0.0" -IsMicOnly + $webUninstallPackage = New-DummyPackageInstallationData -PackageName "web.uninstall.package" -PackageVersion "1.0.0" -IsWebOnly + + Mock -CommandName Get-BadPackages -MockWith { + $packageData.BadMicPackagesToUninstall += @($micUninstallPackage) + $packageData.BadAppPackagesToUninstall += @($webUninstallPackage) + $packageData.BadWebPackagesToUninstall += @($appUninstallPackage) + return $packageData + } + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + It "Removes Bad Packages from App Servers" { + $packages.AppPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.AppPackagesToUninstall | Should -Match $packageData.AppPackagesToUninstall + } + + It "Removes Bad Packages from Mic Servers" { + $packages.MicPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.MicPackagesToUninstall | Should -Match $packageData.MicPackagesToUninstall + } + + It "Removes Bad Packages from Web Servers" { + $packages.WebPackagesToUninstall | Should -Not -BeNullOrEmpty + + $packages.WebPackagesToUninstall | Should -Match $packageData.WebPackagesToUninstall + } + } + + Context "When Getting Install Information" { + $packageData = Init-PackageData + $debugMetadata = Init-DebugMetadata + + $packages, $debugPackagses = Get-ServerPackageInformation -PackageMetadata $packageData -DebugMetadata $debugMetadata ` + -BuildCheckoutDirectory "SomePath" -NugetCredential $fakeCred -AwsProfileName "JunkProfile" -DependencyReleaseValue "junkRelease" + + It "Correctly Labels Has App Installs" { + $packages.HasAppInstalls | Should -BeTrue + } + + It "Correctly Labels Has Mic Installs" { + $packages.HasMicInstalls | Should -BeTrue + } + + It "Correctly Labels Has Web Installs" { + $packages.HasWebInstalls | Should -BeTrue + } + + It "Returns Package Metadata" { + $packages | Should -Not -BeNullOrEmpty + } + + It "Returns DebugMetadata" { + $debugPackagses | Should -Not -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.ps1 b/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.ps1 new file mode 100644 index 0000000..e30f6d4 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Get-ServerPackageInformation.ps1 @@ -0,0 +1,314 @@ +function Get-ServerPackageInformation { + <# + .SYNOPSIS + The bulk of the Classify logic. Determines what versions of which packages get installed. Utilized by Classify-Packages. + + .PARAMETER PackageMetadata + Packages object to populate. + + .PARAMETER DebugMetadata + Meta object used to determine what goes where. + + .PARAMETER BuildCheckoutDirectory + Directory where the code is checked out by TC. Passed through. + + .PARAMETER AwsProfileName + Profile name to get information from AWS. + + .PARAMETER UseV2PackageMetadata + Whether or not to use the new version of the PackageMetadata functions. Passed through. + + .PARAMETER DependencyReleaseValue + The value of the orb release being classified. Passed through. + + #> + [CmdletBinding()] + param( + [Parameter (Mandatory = $true)] + [PSObject]$PackageMetadata, + [Parameter (Mandatory = $true)] + [PSObject]$DebugMetadata, + [Parameter (Mandatory = $true)] + [string]$BuildCheckoutDirectory, + [Parameter (Mandatory = $true)] + [string]$AwsProfileName, + [Parameter (Mandatory = $true)] + [System.Management.Automation.PSCredential]$NugetCredential, + [switch]$UseV2PackageMetadata, + [Parameter (Mandatory = $true)] + [string]$DependencyReleaseValue + ) + + # These were set outside of the region this function came from, but are used only in this function. Nothing happens to the + # underlying data between when they were set and when this function was called, so declare them here instead. + # TODO: Consider moving these values to DebugMetadata + $webInstallNames = $PackageMetadata.WebPackagesToInstall.Name + $webUninstallNames = $PackageMetadata.WebPackagesToUninstall.Name + + $appInstallNames = $PackageMetadata.AppPackagesToInstall.Name + $appUninstallNames = $PackageMetadata.AppPackagesToUninstall.Name + + Write-Host "##teamcity[blockOpened name='Gather Information']" + + $PackageMetadata = Get-EnvironmentData -packageMetadata $PackageMetadata + + # Construct a map of server -> packages installed on that server. + $serverToPackages = @{} + foreach ($server in $PackageMetadata.ServersToQuery) { + # TODO ~ Fix this to not need the !StartsWith part + # ~ Relies on the package classification being right. We can just remove all SREModule types. + $serverToPackages[$server] = (Get-InstalledPackages -ComputerName $server).Where({ !$_.Name.StartsWith("Alkami.Installer") }) + } + + #region Filter existing packages by servertype + # Get the packages from each type of server. + if ($PackageMetadata.HasWebServers) { + $packages = @() + foreach ($server in $PackageMetadata.WebServers) { + $packages += @($serverToPackages[$server]) + } + $DebugMetadata.WebServerPackages = @(Select-UniqueServerPackages $packages) + } + + if ($PackageMetadata.HasAppServers) { + $packages = @() + foreach ($server in $PackageMetadata.AppServers) { + $packages += @($serverToPackages[$server]) + } + $DebugMetadata.AppServerPackages = @(Select-UniqueServerPackages $packages) + } + + if ($PackageMetadata.HasMicServers) { + $packages = @() + foreach ($server in $PackageMetadata.MicServers) { + $packages += @($serverToPackages[$server]) + } + $DebugMetadata.MicServerPackages = @(Select-UniqueServerPackages $packages) + } + + if ($PackageMetadata.HasFabServers) { + $packages = @() + foreach ($server in $PackageMetadata.FabServers) { + $packages += @($serverToPackages[$server]) + } + $DebugMetadata.FabServerPackages = @(Select-UniqueServerPackages $packages) + } + #endregion Filter existing packages by servertype + + #region Find all packages with multi-server-existing-version-mismatch for true-up + # Create a list of package name -> versions installed on each server. + # This gives a view of all of the versions of a particular package installed. + foreach ($serverPackages in $serverToPackages.Values) { + foreach ($package in $serverPackages) { + $packageNameLower = $package.Name.ToLower() + if (!$PackageMetadata.PackageToVersions.ContainsKey($packageNameLower)) { + $PackageMetadata.PackageToVersions[$packageNameLower] = @($package.Version) + } else { + $PackageMetadata.PackageToVersions[$packageNameLower] = $PackageMetadata.PackageToVersions[$packageNameLower] + $package.Version + } + } + } + # Limit the versions down to unique versions installed. + $packageToVersionsKeys = @() + $PackageMetadata.PackageToVersions.Keys; # So we aren't "changing" the keys of the map and breaking enumeration. + foreach ($key in $packageToVersionsKeys) { + $PackageMetadata.PackageToVersions[$key] = ($PackageMetadata.PackageToVersions[$key] | Select-Object -Unique) + } + #endregion Find all packages with multi-server-existing-version-mismatch for true-up + + # Create a list of package names to servers the package is installed on. + # Also, when a FAB name is encountered, add the entire list of FAB servers. + foreach ($server in $serverToPackages.Keys) { + $serversToAdd = @($server) + if ($server -like "fab*") { + $serversToAdd = @($PackageMetadata.FabServers) + } + $serverPackages = $serverToPackages[$server] + foreach ($package in $serverPackages) { + $lowerName = $package.Name.ToLower() + if (!$PackageMetadata.PackageToServers.ContainsKey($lowerName)) { + $PackageMetadata.PackageToServers[$lowerName] = $serversToAdd + } else { + $PackageMetadata.PackageToServers[$lowerName] += $serversToAdd + } + } + } + + #region Filter packages down to unique packages across the entire environment + $allWebTierPackages = (Select-UniqueServerPackages $DebugMetadata.WebServerPackages) + $allAppTierPackages = (Select-UniqueServerPackages ( + @($DebugMetadata.AppServerPackages) + + @($DebugMetadata.MicServerPackages) + + @($DebugMetadata.FabServerPackages) + )) + + # Create a map of the packages getting installed from the job parameters. We need to know which packages came from the deploy params, and which from the servers. + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.WebPackagesToInstall)) { + foreach ($package in $PackageMetadata.WebPackagesToInstall) { + $DebugMetadata.WebPackagesToInstallMap[$package.Name.ToLower()] = $package + } + } + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.AppPackagesToInstall)) { + foreach ($package in $PackageMetadata.AppPackagesToInstall) { + $DebugMetadata.AppPackagesToInstallMap[$package.Name.ToLower()] = $package + } + } + #endregion Filter packages down to unique packages across the entire environment + + #region Determine $all___TierPackageInstalls + if ($PackageMetadata.ForceReinstallPackages) { + # It's a full deploy. (This is an implicit reproduction of the .IsOrbDeploy flag. The two should probably be consolidated at some point.) + # Overwrite the packages from the environment with packages intended to be installed. + # This will leave us with a list of things we want installed, plus the things we might force-reinstall. + $allWebTierPackages = (Select-InstallPackages $allWebTierPackages $PackageMetadata.WebPackagesToInstall) + $allAppTierPackages = (Select-InstallPackages $allAppTierPackages $PackageMetadata.AppPackagesToInstall) + } else { + # Otherwise it's an element deploy. + # Filter down the list of packages to the ones that are being deployed. + $allWebTierPackages = $PackageMetadata.WebPackagesToInstall + $allAppTierPackages = $PackageMetadata.AppPackagesToInstall + } + #endregion Determine $all___TierPackageInstalls + + #region Exclude chocolatey if present + if ($null -ne ($allWebTierPackages | Where-Object { $_.Name -eq "chocolatey" })) { + Write-Warning "The Chocolatey Package Manager Program package (chocolatey) was passed in to be installed on the Web Tier. At this time we explicitly dissallow that. It has been skipped." + } + if ($null -ne ($allAppTierPackages | Where-Object { $_.Name -eq "chocolatey" })) { + Write-Warning "The Chocolatey Package Manager Program package (chocolatey) was passed in to be installed on the App Tier. At this time we explicitly dissallow that. It has been skipped." + } + + # Remove chocolatey from the install lists. + $allWebTierPackages = $allWebTierPackages | Where-Object { $_.Name -ne "chocolatey" } + $allAppTierPackages = $allAppTierPackages | Where-Object { $_.Name -ne "chocolatey" } + + #endregion Exclude chocolatey if present + + + #region Remove PackagesToUninstall from all***TierPackages + #TODO: Independent filter here or add to Select-InstallPackages called from Line 636-637 ? + #TODO: OR Add to Get-PackageInstallationData in Alkami.Powershell.Choco called from 675-677 ? + #endregion Remove PackagesToUninstall from all***TierPackages + + $buildMappingArgs = @{ + AllAppTierPackages = $allAppTierPackages + AllWebTierPackages = $allWebTierPackages + BuildCheckoutDirectory = $BuildCheckoutDirectory + DebugMetadata = $DebugMetadata + NugetCredential = $NugetCredential + PackageMetadata = $PackageMetadata + UseV2PackageMetadata = $UseV2PackageMetadata + DependencyReleaseValue = $DependencyReleaseValue + } + + $PackageMetadata,$DebugMetadata = Get-PackageServerMapping @buildMappingArgs + + $PackageMetadata = Get-BadPackages -DebugMetadata $DebugMetadata -PackageMetadata $PackageMetadata + + # Strip out empty/null entries. Follow up/remove with SRE-17113. Suggestion for debugging; move this block around and see where it fails + [array]$PackageMetadata.WebPackagesToInstall = $PackageMetadata.WebPackagesToInstall.Where({$null -ne $_}) + [array]$PackageMetadata.AppPackagesToInstall = $PackageMetadata.AppPackagesToInstall.Where({$null -ne $_}) + [array]$PackageMetadata.MicPackagesToInstall = $PackageMetadata.MicPackagesToInstall.Where({$null -ne $_}) + + #region Doublecheck force-reinstall + # Keep only the packages that need to be force reinstalled, or that came from the install parameters. + if ($PackageMetadata.ForceReinstallPackages) { + $PackageMetadata.WebPackagesToInstall = $PackageMetadata.WebPackagesToInstall | Where-Object { + # Where package exists on a server, and will be deleted (ie: widget), but isn't in either of our lists. + (($_.ForceSameVersion -and ($webUninstallNames -inotcontains $_.Name -and $webInstallNames -inotcontains $_.Name))) -or + # Packages we're installing for some reason decided upon above. + ($DebugMetadata.WebPackagesToInstallMap.ContainsKey($_.Name.ToLower())) } + $PackageMetadata.AppPackagesToInstall = $PackageMetadata.AppPackagesToInstall | Where-Object { + (($_.ForceSameVersion -and ($appUninstallNames -inotcontains $_.Name -and $appInstallNames -inotcontains $_.Name))) -or + # Packages we're installing for some reason decided upon above. + ($DebugMetadata.AppPackagesToInstallMap.ContainsKey($_.Name.ToLower())) } + } + #endregion Doublecheck force-reinstall + + #region Remove packages from the install lists that do not need to be needlessly re-installed. + Write-Host ("##teamcity[blockOpened name='Removing Already-Installed Packages']") + # Web/App package arrays here are getting null entries from *mumble mumble* somewhere... + # cut out the nulls before passing it along. + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.WebPackagesToInstall)) { + $packagesToInstall = @() + foreach($package in $PackageMetadata.WebPackagesToInstall){ + if($null -ne $package){ + $packagesToInstall += $package + } + } + + $PackageMetadata.WebPackagesToInstall = (Remove-PackagesThatAreAlreadyInstalled -packagesToInstall $packagesToInstall -packageMetadata $PackageMetadata -debugMetadata $DebugMetadata) + } + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.AppPackagesToInstall)) { + $packagesToInstall = @() + foreach($package in $PackageMetadata.AppPackagesToInstall){ + if($null -ne $package){ + $packagesToInstall += $package + } + } + $PackageMetadata.AppPackagesToInstall = (Remove-PackagesThatAreAlreadyInstalled -packagesToInstall $packagesToInstall -packageMetadata $PackageMetadata -debugMetadata $DebugMetadata) + } + + # $PackageMetadata.WebPackagesToInstall = (Remove-PackagesThatAreAlreadyInstalled -packagesToInstall $PackageMetadata.WebPackagesToInstall -packageMetadata $PackageMetadata -debugMetadata $DebugMetadata) + # $PackageMetadata.AppPackagesToInstall = (Remove-PackagesThatAreAlreadyInstalled -packagesToInstall $PackageMetadata.AppPackagesToInstall -packageMetadata $PackageMetadata -debugMetadata $DebugMetadata) + Write-Host ("##teamcity[blockClosed name='Removing Already-Installed Packages']") + #endregion Remove packages from the install lists that do not need to be needlessly re-installed. + + #region Gather AWS settings from a sample server + # Had to re-obtain this value here when functionalizing classify. + $serverToQuery = $packageMetadata.ServersToQuery | Select-Object -First 1 + + # Grab data from first server available + $awsSettings = Get-AwsSettings -serverToTest $serverToQuery -profileName $AwsProfileName + + $PackageMetadata.AwsSettings = $awsSettings + + #endregion Gather AWS settings from a sample server + + #region Separate App-Mic/Fab installs and uninstalls. + # Separate out the mic/fab installs from the app installs. + [array]$PackageMetadata.MicPackagesToInstall = $PackageMetadata.AppPackagesToInstall | Where-Object { $_.InstallToMic -or $_.InstallToFab } + [array]$PackageMetadata.AppPackagesToInstall = $PackageMetadata.AppPackagesToInstall | Where-Object { $_.InstallToApp } + + $newAppUninstalls = @() + $newMicUninstalls = @() + + foreach($package in $PackageMetadata.AppPackagesToUninstall) { + $packageInstalledOnServers = $PackageMetadata.PackageToServers[$package.Name] + $installedOnAppServers = $null -ne (Select-AlkamiAppServers -servers $packageInstalledOnServers) + $installedOnMicServers = ($null -ne (Select-AlkamiMicServers -servers $packageInstalledOnServers)) -or + ($null -ne (Select-AlkamiFabServers -servers $packageInstalledOnServers)) + + if($installedOnAppServers) { + $newAppUninstalls += $package + } + if($installedOnMicServers) { + $newMicUninstalls += $package + } + } + $PackageMetadata.AppPackagesToUninstall = $newAppUninstalls + $PackageMetadata.MicPackagesToUninstall = $newMicUninstalls + + # Add the bad packages to uninstall to the uninstall lists. + # This must happen underneath the app/mic uninstall separation, so we don't remove a valid package from a mic/app server. + # This preserves explicit package uninstalls, but allows for specific removal of bad packages. + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.BadWebPackagesToUninstall)) { $PackageMetadata.WebPackagesToUninstall += $PackageMetadata.BadWebPackagesToUninstall; } + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.BadAppPackagesToUninstall)) { $PackageMetadata.AppPackagesToUninstall += $PackageMetadata.BadAppPackagesToUninstall; } + if (!(Test-IsCollectionNullOrEmpty $PackageMetadata.BadMicPackagesToUninstall)) { $PackageMetadata.MicPackagesToUninstall += $PackageMetadata.BadMicPackagesToUninstall; } + + #endregion Separate Mic/Fab installs/uninstalls. + + $packageMetadata, $debugMetadata = Add-OldHotfixPackagesToUninstallList -DependencyReleaseValue $DependencyReleaseValue -PackageMetadata $packageMetadata -DebugMetadata $debugMetadata + + #region Final determination of what types of installs we are doing + $PackageMetadata.HasWebInstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.WebPackagesToInstall) + $PackageMetadata.HasWebUninstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.WebPackagesToUninstall) + $PackageMetadata.HasAppInstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.AppPackagesToInstall) + $PackageMetadata.HasAppUninstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.AppPackagesToUninstall) + $PackageMetadata.HasMicInstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.MicPackagesToInstall) + $PackageMetadata.HasMicUninstalls = !(Test-IsCollectionNullOrEmpty $PackageMetadata.MicPackagesToUninstall) + #endregion Final determination of what types of installs we are doing + + return $PackageMetadata, $DebugMetadata + + Write-Host ("##teamcity[blockClosed name='Gather Information']") +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.ps1 new file mode 100644 index 0000000..db40b36 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.ps1 @@ -0,0 +1,158 @@ +function Install-NewRelicDotNetAgent { +<# +.SYNOPSIS + Installs the New Relic .NET Agent from the Default Chocolatey Feed +.PARAMETER EnvironmentKey + Value used to obtain the License Key for the Chocolatey upgrade +.PARAMETER Source + Where the package is located in +.PARAMETER Version + Specified package version number of New Relic .Net +#> + + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Scope = 'Function', Justification = "This is not evaluating user-supplied input")] + Param( + [Parameter(Mandatory = $true)] + [Alias("Environment")] + [string]$EnvironmentKey, + + [Parameter(Mandatory = $false)] + [string]$Source = $null, + + [Parameter(Mandatory = $false)] + [string]$Version = $null + ) + + $logLead = (Get-LogLeadName) + $newRelicDotnetAgentPackageName = "newrelic-dotnet" + $installedAppsRegistryKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" + $alkamiFeedMatch = "http?://packagerepo.orb.alkamitech.com/nuget/choco.*" + + $packageSource = $Source + if ([string]::IsNullOrWhiteSpace($packageSource)) { + + Write-Host "$logLead : Package Source not provided. Iterating over configured feeds to look for the package to install." + [array]$sources = (Get-ChocolateySources) | Where-Object { + $_.Source -like $alkamiFeedMatch -and [bool]$_.IsSDK -eq $false -and [bool]$_.Disabled -eq $false + } + + Write-Verbose "$logLead : Found $($sources.Count) active feeds matching $alkamiFeedMatch" + + if (Test-IsCollectionNullOrEmpty -Collection $sources) { + + Write-Error "$logLead : NOT FOUND - Alkami choco feed for $newRelicDotnetAgentPackageName" + return + + } else { + + foreach ($feedSource in $sources) { + + Write-Verbose "$logLead : Checking source with name $($feedSource.Name) and Feed URL $($feedSource.Source)" + $highestAvailableChoco = Get-ChocoState -exact -packageName $newRelicDotnetAgentPackageName -source $feedSource.Source -ErrorAction Continue + if ($null -eq $highestAvailableChoco) { + + Write-Warning "$logLead : Package $newRelicDotnetAgentPackageName not found at feed $($feedSource.Source)" + + } else { + + Write-Host "$logLead : Package $newRelicDotnetAgentPackageName found at $($feedSource.Source). Using this source" + $packageSource = $feedSource.Source + continue + } + } + + if ($null-eq $packageSource) { + + Write-Error "$logLead : No configured feed has package $newRelicDotnetAgentPackageName available. Execution cannot continue." + return + } + } + } else { + + Write-Host "$logLead : Using provided Package Source - $packageSource" + } + + $licenseKey = (Get-NewRelicAccountDetails $EnvironmentKey).LicenseKey + Write-Verbose ("$logLead : Using license key {0}" -f $licenseKey) + + $localChoco = Get-ChocoState -local -exact -packageName $newRelicDotnetAgentPackageName + Write-Verbose "$logLead : LocalChoco : $localChoco" + + $doInstall = $false + if (Test-StringIsNullOrWhiteSpace -Value $Version) { + + #TODO: Add trap for failure to find any version of NRDNA in the feed + if (Test-IsCollectionNullOrEmpty -Collection $highestAvailableChoco) { + Write-Error "$logLead : PACKAGE NOT FOUND - Package [$newRelicDotnetAgentPackageName] in feed [$packageSource]" + return + } else { + Write-Host "$loglead : No specific version specified, upgrading/installing latest available" + Write-Verbose "$loglead : Highest version found in feed: $highestAvailableChoco" + } + [System.Version]$highestAvailableChocoVersion = ((Select-Object -InputObject $highestAvailableChoco -First 1).Version) + [System.Version]$versionToInstall = $highestAvailableChocoVersion + } else { + try { + $versionObject = Get-VersionPSObject -Version $Version -ErrorAction Stop + Write-Host "$loglead : Version specified : $Version" + [System.Version]$versionToInstall = $versionObject.Version + } catch { + Write-Warning "$loglead : Version specified not in accordance to correct format : $Version" + throw $_ + } + } + + if ($null -eq $versionToInstall) { + $errorMessage = "$loglead : Version number is empty, exiting..." + throw $errorMessage + } + + $isAgentInstalledViaChoco = !(Test-IsCollectionNullOrEmpty -Collection $localChoco) + + Write-Verbose "$logLead : Is .Net agent Installed via Choco package? $isAgentInstalledViaChoco" + + if ( !$isAgentInstalledViaChoco ) { + $installedApps = Get-ItemProperty -Path $installedAppsRegistryKey ` + | Select-Object DisplayName, DisplayVersion, InstallDate ` + | Where-Object { $_.DisplayName -match "New Relic .NET" } + Write-Verbose "$logLead : Value from registry key : $installedApps" + + $isAgentInstalled = ![string]::IsNullOrWhiteSpace($installedApps) + + if ( !($isAgentInstalled) ) { + $doInstall = $true + Write-Host "$logLead : Determined no .Net Agent is installed." + } + } else { + [System.Version]$preInstallChocoVersion = ((Select-Object -InputObject $localChoco -First 1).Version) + if ($versionToInstall -gt $preInstallChocoVersion) { + $doInstall = $true + Write-Host "$logLead : Determined .Net Agent is installed via Choco and requires an update." + Write-Host "$logLead : Installed : $preInstallChocoVersion" + Write-Host "$logLead : Version to install : $versionToInstall" + } elseif ($versionToInstall -lt $preInstallChocoVersion) { + $doInstall = $true + Write-Host "$logLead : Determined .Net Agent is installed via Choco and requires a downgrade." + Write-Host "$logLead : Installed : $preInstallChocoVersion" + Write-Host "$logLead : Version to install : $versionToInstall" + Uninstall-NewRelicDotNetAgent + } + } + + if ($doInstall) { + Write-Host "$logLead : Install package : $newRelicDotnetAgentPackageName" + Write-Host "$logLead : Package version : $versionToInstall" + $command = "choco upgrade -y $newRelicDotnetAgentPackageName --version $versionToInstall --no-progress --failstderr --params `"'license_key=$licenseKey'`" --source $packageSource" + Write-Host "Command: $command" + Invoke-Expression -Command $command + } else { + Write-Host "$logLead : No Update Was Performed" + } + + #TODO: Move these inside the $doInstall if-block? + # These currently run all the time if the new early returns don't get hit + # Even if we write-host "No Update Was Performed" + Copy-NewRelicCustomInstrumentationFiles + Set-NewRelicConfigurationValues +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.tests.ps1 new file mode 100644 index 0000000..0f9f2c8 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicDotNetAgent.tests.ps1 @@ -0,0 +1,477 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +# NOTE: This test file is a hot mess. It is a product of trying to combine User Story changes +# with mockability refactors and suffers as a result. I believe it does what it "should" +# do, with valid PASS/FAIL assertions and criteria; HOWEVER, it should be simplified and +# cleaned up, at a later date. Consider this my apology to future-us. +# trowton - 2020-10-29 +# NOTE: I got tired of this always failing for no good reason, so I made it sorta-good-enough-pass-for-now +# I did not walk the actual code for "good" - I also apologize to future us - cbrand 2021-05-06 +# TODO: Double check various Contexts to ensure all external calls, especially network calls +# are mocked - Get-ChocoState, especially. Get-ChocoSources should also be mocked in all +# Contexts +# NOTE: Mocks are not scoped to It - only Describe/Context - Mocks at It level will +# impact other, later Its in the same Context, or Describe if Contexts are not used + +# FAKES AND DUMMIES +$nrdnAgentPackageName = "newrelic-dotnet" +$nrdnAgentDowngradeVersion = "0.9.9.9" +$nrdnAgentPriorVersion = "1.0.0.0" +$nrdnAgentNewVersion = "1.1.0.0" + +$priorVersionProperties = @{ Name = $nrdnAgentPackageName; Version = $nrdnAgentPriorVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null } +$testFake_PriorPackage = New-Object -TypeName PSObject -Prop $priorVersionProperties + +$newVersionProperties = @{ Name = $nrdnAgentPackageName; Version = $nrdnAgentNewVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null } +$testFake_NewPackage = New-Object -TypeName PSObject -Prop $newVersionProperties + +$infraAgentDisplayName = "New Relic Infrastructure Agent" +$nrdnAgentDisplayName = "New Relic .NET Agent (64-bit)" +$fakeDisplayName = "Fake .NET App" + +# Registry FAKES +$infraAgentRegistryProps = @{ DisplayName = $infraAgentDisplayName; DisplayVersion = "9.9.9"; InstallDate = "20000101" } +$nrdnAgentRegistryProps = @{ DisplayName = $nrdnAgentDisplayName; DisplayVersion = "42.42.42.42"; InstallDate = "20200101" } +$dummyAppRegistryProps = @{ DisplayName = $fakeDisplayName; DisplayVersion = "8.8.8"; InstallDate = "20010101" } +$infraAgentFakeApp = New-Object -TypeName PSObject -Prop $infraAgentRegistryProps +$nrdnAgentFakeApp = New-Object -TypeName PSObject -Prop $nrdnAgentRegistryProps +$dummyAppFakeApp = New-Object -TypeName PSObject -Prop $dummyAppRegistryProps +$fakeInstalledApps_NRDNA_TRUE = @($infraAgentFakeApp, $nrdnAgentFakeApp, $dummyAppFakeApp) +$fakeInstalledApps_NRDNA_FALSE = @($infraAgentFakeApp, $dummyAppFakeApp) + +# ChocolateySource FAKE +$fakeFeedSource = "https://packagerepo.orb.alkamitech.com/nuget/choco.FAKE_FEED_FOR_UNIT_TEST" +$fakeFeed = @{ Source = $fakeFeedSource; IsSDK = $false; Disabled = $false } +$fakeFeedList = @($fakeFeed) +Describe -Name "Code_Paths" -Fixture { + # TODO: This being missing causes some stuff to try to install on local tests + # TODO: This needs to be mocked more properly + # dummy for catching in case it's not mocked as it should be + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + param ([Alias("local")][switch]$LocalOnly, $exact, $packageName, $source) + if ($localonly) { + return $testFake_PriorPackage + } else { + return $testFake_NewPackage + } + } + + #Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith {} + + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock -MockWith { + return @{LicenseKey = "FAKE-KEY-FOR-UNIT-TEST" } + } + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Invoke-Expression -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Uninstall-NewRelicDotNetAgent -ModuleName $moduleForMock -MockWith {} + + Context -Name "Parameters" -Fixture { + # ARRANGE + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + # $alkamiFeedMatch = "https://packagerepo.orb.alkamitech.com/nuget/choco.*"; + # $sdkFeedMatch = "https://feeds.alkamitech.com/nuget/*"; + # $fakeFeedList = ($fakeFeedList | Sort-Object -Property @{Expression={ [int]($_.Source -like $alkamiFeedMatch) * 2 + [int]($_.Source -like $sdkFeedMatch)}} -Descending); + return $fakeFeedList + } + + It -Name "Choco_Source_Provided" -Test { + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Source "FAKE SOURCE FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ChocolateySources -Times 0 -Exactly -Scope It + } + + It -Name "Choco_Source_Not_Provided" -Test { + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ChocolateySources -Times 1 -Exactly -Scope It + } + + It -Name "Choco_Source_Not_Provided_Valid_Feed_Found" -Test { + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ChocolateySources -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -ParameterFilter { + $Message -match "NOT FOUND - Alkami choco feed" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + + } + + It -Name "Version_Not_Provided" -Test { + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Command -match "choco upgrade -y newrelic-dotnet --version 1.1.0.0 --no-progress" + } + # This value contains a version, which is populated from line 55 where Get-ChocoState is mocked to the object + # $newVersionProperties on line 30 + # This happens when a $Version is not provided + + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + + It -Name "Version_Provided" -Test { + + # ARRANGE + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { @{ Name = $nrdnAgentPackageName; Version = "1.2.4.8"; } } + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Version "1.2.4.8" + + # ASSERT + Assert-MockCalled -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Value -eq "1.2.4.8" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Command -match "choco upgrade -y newrelic-dotnet --version 1.2.4.8" + } + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + + It -Name "Version_Bad_Format" -Test { + + # ARRANGE + $badVersionProperties = @{ Version = "INVALIDVERSION"; } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { $badVersionProperties } + + # ACT + { Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Version "INVALIDVERSION" } | Should -Throw + + # ASSERT + Assert-MockCalled -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Value -eq "INVALIDVERSION" + } + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -match "Version specified not in accordance to correct format" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + + It -Name "Version_Null" -Test { + + # ARRANGE + $badVersionProperties = @{ Version = $null; } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { $badVersionProperties } + + # ACT + { Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Version $null } | Should -Throw + + # ASSERT + Assert-MockCalled -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -match "Version specified not in accordance to correct format" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + + It -Name "Choco_Source_Not_Provided_No_Feed_Found" -Test { + # ARRANGE + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return @() + } + + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ChocolateySources -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ParameterFilter { + $Message -match "NOT FOUND - Alkami choco feed" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + } + + Context -Name "When different Versions are passed thru Get-VersionPSObject" { + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { return $fakeFeedList } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + + It -Name "It throws when given an invalid version number" -Test { + # ARRANGE + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { throw } -ParameterFilter { $Version -eq "hello" } + + # ACT + { Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Version "hello" } | Should -Throw + + # ASSERT + Assert-MockCalled -CommandName Get-VersionPSObject -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Message -match "Version specified not in accordance to correct format : hello" + } + } + + It -Name "It doesn't throw when given a valid version number" -Test { + # ARRANGE + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { @{ Name = $nrdnAgentPackageName; Version = "9.88.44.0"; } } -ParameterFilter { $Version -eq "9.88.44.0" } + + # ACT + { Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" -Version "9.88.44.0" } | Should -Not -Throw + + # ASSERT + Assert-MockCalled -CommandName Get-VersionPSObject -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter { + $Object -match "Version specified : 9.88.44.0" + } + } + } + + Context -Name "Installed_Current_Version" -Fixture { + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return $fakeFeedList + } + + It -Name "Via_Choco" -Test { + # ARRANGE + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + # Newest version installed via choco + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + param ([switch]$local, [switch]$exact, $packageName, $source) + if ($local) { + return $testFake_NewPackage + } else { + return $testFake_NewPackage + } + } + # Baseline - already installed via choco + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { + return $false + } + # Baseline - install version matches highest available version + Mock -CommandName Select-Object -ModuleName $moduleForMock -MockWith { + return $newVersionProperties + } -ParameterFilter { $InputObject -eq $testFake_NewPackage -and $First -eq 1 } + + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "Agent is installed via Choco and requires an update" + } -Times 0 -Exactly + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + } + + Context -Name "Not_Installed" -Fixture { + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return $fakeFeedList + } + + It -Name "Anywhere" -Test { + # ARRANGE + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + # Not installed via choco + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $null + } -ParameterFilter { $local } + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_NewPackage + } -ParameterFilter { -not $local } + # Not installed outside choco + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { + return $fakeInstalledApps_NRDNA_FALSE + } + + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ChocoState -ModuleName $moduleForMock -ParameterFilter { + $local + } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ChocoState -ModuleName $moduleForMock -ParameterFilter { + -not $local + } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ItemProperty -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "Determined no .Net Agent is installed" + } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "Install package :" + } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { + $Command -match "choco upgrade" + } -Times 1 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + } + + Context -Name "Installed" -Fixture { + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return $fakeFeedList + } + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + It -Name "Outside_of_Choco" -Test { + # ARRANGE + # Not installed via choco + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $null + } -ParameterFilter { $local } + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_NewPackage + } -ParameterFilter { -not $local } + # Installed outside choco + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { + return $fakeInstalledApps_NRDNA_TRUE + } + + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Get-ItemProperty -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { + $Command -match "choco upgrade" + } -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "No Update Was Performed" + } -Times 1 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + } + + Context -Name "Installed_Prior_Version" -Fixture { + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return $fakeFeedList + } + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + It -Name "Via_Choco" -Test { + # ARRANGE + # Not installed via choco + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_PriorPackage + } -ParameterFilter { $local } + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_NewPackage + } -ParameterFilter { -not $local } + # Installed outside choco + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith {} + + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { + $Command -match "choco upgrade" + } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "Install package :" + } -Times 1 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + } + + Context -Name "Package_Not_Found_In_Feed" -Fixture { + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { + return $fakeFeedList + } + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + # Newest version installed via choco + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_NewPackage + } -ParameterFilter { $local } + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $null + } -ParameterFilter { -not $local } + + It -Name "Error_And_Return" -Test { + # ACT + Install-NewRelicDotNetAgent -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ParameterFilter { + $Message -match "PACKAGE NOT FOUND - Package" + } + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + # !These are ALWAYS called, even if Agent is installed via choco and is current! + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + + } + + } + + Context -Name "Version_Downgrade" -Fixture { + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return $testFake_PriorPackage + } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + Mock -CommandName Get-ChocolateySources -ModuleName $moduleForMock -MockWith { return $fakeFeedList } + Mock -CommandName Get-VersionPSObject -ModuleName $moduleForMock -MockWith { @{ Name = $nrdnAgentPackageName; Version = $nrdnAgentDowngradeVersion; } } + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + + It -Name "Downgrade_Uninstall" -Test { + + # ARRANGE + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { $false } + + # ACT + Install-NewRelicDotNetAgent -Version $nrdnAgentDowngradeVersion -environmentKey "FAKE ENV FOR UNIT TEST" + + # ASSERT + Assert-MockCalled -CommandName Uninstall-NewRelicDotNetAgent -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Invoke-Expression -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Copy-NewRelicCustomInstrumentationFiles -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-NewRelicConfigurationValues -ModuleName $moduleForMock -Times 1 -Exactly -Scope It + } + } +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicInfrastructure.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicInfrastructure.ps1 new file mode 100644 index 0000000..1881213 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicInfrastructure.ps1 @@ -0,0 +1,44 @@ +function Install-NewRelicInfrastructure { +<# +.SYNOPSIS + +Installs the New Relic Infrastructure Monitor + +.PARAMETER serverRole + +The role of the Server "App,Web,DB,Report,Redis,Entrust" + +.PARAMETER pod + +The pod the server is in + +.PARAMETER environmentKey + +The environment. This usually comes from the job in teamcity + +#> + [CmdletBinding()] + Param( + + [Parameter(Mandatory = $true)] + [string]$serverRole, + + [Parameter(Mandatory = $true)] + [string]$pod, + + [Parameter(Mandatory = $false)] + [Alias("Environment")] + [string]$environmentKey + ) + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : Choco Installing New Relic Infra") + + choco upgrade newrelic-infra -y --no-progress + + Write-Host ("$logLead : Choco Installation completed") + + Set-InfrastructureConfiguration $serverRole $pod $environmentKey + Start-Service newrelic-infra +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicServerMonitor.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicServerMonitor.ps1 new file mode 100644 index 0000000..c734383 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-NewRelicServerMonitor.ps1 @@ -0,0 +1,33 @@ +function Install-NewRelicServerMonitor { +<# +.SYNOPSIS + +Installs the New Relic Server Monitor +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("Environment")] + [string]$environmentKey + ) + + $logLead = (Get-LogLeadName); + + $msiName = "NewRelicServerMonitor_x64.msi" + $64bitMSIInstallerUrl = "https://download.newrelic.com/windows_server_monitor/release/NewRelicServerMonitor_x64_3.3.6.0.msi" + + $folderDateFormat = Get-Date -Format ddMMyyyy + $outputFolder = ("C:\Temp\Deploy\NRServerMonitor_{0}" -f $folderDateFormat) + + $downloadedMsi = Get-PackageForInstallation $loglead $outputFolder $msiName $64bitMSIInstallerUrl + + $licenseKey = (Get-NewRelicAccountDetails $environmentKey).LicenseKey + Write-Verbose ("$logLead : Using license key {0}" -f $licenseKey) + + $installParams = ("/i {0} /qn NR_LICENSE_KEY=`"{1}`"" -f $downloadedMsi, $licenseKey) + Write-Verbose ("$logLead : Running msiexec.exe with parameters {0}" -f $installParams) + + $installProcess = Start-Process msiexec.exe -ArgumentList $installParams -NoNewWindow -PassThru -Wait + Write-Output ("$logLead : Installation completed with exit code {0}" -f $installProcess.ExitCode) +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-ORB.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-ORB.ps1 new file mode 100644 index 0000000..54d7b24 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-ORB.ps1 @@ -0,0 +1,116 @@ +function Install-ORB { +<# +.SYNOPSIS + +Provides an entry point to install ORB on a Web or App Tier Server. This function is primarily utilized by automation which may not know what tier it may be installing. +.DESCRIPTION + +Provides an entry point to install ORB on a web or app tier server. In order to leverage this function the machine must be properly instrumented with a ServerRole envirornmental variable, if it is a +new machine. The ServerRole system environmental variable value should be set to one of either Web or App. If your machines have not been tagged during provisioning you may add the variable manually, +or call the Install-ORBAppServer / Install-ORBWebServer functions directly with the appropriate parameters +.PARAMETER clientUrl + +Alias: ClientSiteUrl +Provide the Client Site URL without preceeding protocol or trailing slash. For example, instead of https://orb.alkamitech.com/, provide orb.alkamitech.com. This parameter is only used when installing on a web tier server. +.PARAMETER adminUrl + +Alias: AdminSiteUrl +Provide the Admin Site URL without preceeding protocol or trailing slash. For example, instead of https://admin-orb.alkamitech.com/, provide admin-orb.alkamitech.com. This parameter is only used when installing on a web tier server. +.PARAMETER ipstsUrl + +Alias: IPSTSSiteUrl +Provide the IPSTS Site URL without preceeding protocol or trailing slash. For example, instead of https://orb-ip.alkamitech.com/, provide orb-ip.alkamitech.com. This parameter is only used when installing on a web tier server. +.PARAMETER skipSiteAndAppWarmup + +Alias: SkipWarmup +Will skip the warmup of sites or services when set +.PARAMETER doCombineAdminAppPools + +Alias: CombineAdminAppPools +Will configure admin websites to use an app pool named 'Admin' when set +.PARAMETER doCombineClientAppPools + +Alias: CombineClientAppPools +Will configure client websites to use an app pool named 'WebClient' when set +.PARAMETER doCombineIPSTSAppPools + +Alias: CombineIPSTSAppPools +Will configure IPSTS websites to use an app pool named 'IPSTS' when set +.EXAMPLE + +Install-ORB -ClientSiteUrl orb.alkamitech.com -AdminSiteUrl admin-orb.alkamitech.com -IpstsSiteUrl orb-ip.alkamitech.com -SecretServerUserName foobar -SecretServerPassword barfoo -SecretServerFolders "POD6,Common" +.EXAMPLE + +Install-ORB -Broadcasters "Server1,Server2,Server3" -SkipWarmup +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("ClientSiteUrl")] + [string]$clientUrl, + + [Parameter(Mandatory = $false)] + [Alias("AdminSiteUrl")] + [string]$adminUrl, + + [Parameter(Mandatory = $false)] + [Alias("IPSTSSiteUrl")] + [string]$ipstsUrl, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerUserName")] + [string]$secretUserName, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerPassword")] + [string]$secretPassword, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerFolders")] + [string]$secretFolderNames, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerDomain")] + [string]$secretDomain = "corp.alkamitech.com", + + + [Parameter(Mandatory = $false)] + [Alias("SkipWarmup")] + [switch]$skipSiteAndAppWarmup, + + [Parameter(Mandatory = $false)] + [Alias("CombineAdminAppPools")] + [switch]$doCombineAdminAppPools, + + [Parameter(Mandatory = $false)] + [Alias("CombineClientAppPools")] + [switch]$doCombineClientAppPools, + + [Parameter(Mandatory = $false)] + [Alias("CombineIPSTSAppPools")] + [switch]$doCombineIPSTSAppPools + ) + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : Received Arguments:"); + Write-Verbose (" ClientSiteUrl: $clientUrl"); + Write-Verbose (" AdminSiteUrl: $adminUrl"); + Write-Verbose (" IpstsSiteUrl: $ipstsUrl"); + Write-Verbose (" SecretServerUserName: $secretUserName"); + Write-Verbose (" SecretServerPassword: REDACTED"); + Write-Verbose (" SecretServerFolders: $secretFolderNames"); + Write-Verbose (" SecretServerDomain: $secretDomain"); + + if (Test-IsAppServer) { + Install-ORBAppServer -SecretServerUserName $secretUserName -SecretServerPassword $secretPassword -SecretServerFolders $secretFolderNames -SecretServerDomain $secretDomain -SkipAppWarmup:$skipSiteAndAppWarmup; + } + elseif (Test-IsWebServer) { + Install-ORBWebServer -ClientSiteUrl $clientUrl -AdminSiteUrl $adminUrl -IpstsSiteUrl $ipstsUrl -SecretServerUserName $secretUserName -SecretServerPassword $secretPassword -SecretServerFolders $secretFolderNames -SecretServerDomain $secretDomain -SkipSiteWarmup:$skipSiteAndAppWarmup -CombineAdminAppPools:$doCombineAdminAppPools -CombineClientAppPools:$doCombineClientAppPools -CombineIPSTSAppPools:$doCombineIPSTSAppPools; + } + else { + Write-Warning "Could not automatically determine if this server is a web or app tier machine."; + Write-Output "You may still call Install-ORBAppServer or Install-ORBWebServer directly"; + } +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-ORBAppServer.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-ORBAppServer.ps1 new file mode 100644 index 0000000..4c588f0 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-ORBAppServer.ps1 @@ -0,0 +1,94 @@ +function Install-ORBAppServer { +<# +.SYNOPSIS + Installs an ORB App Server. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("SecretServerUserName")] + [string]$secretUserName, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerPassword")] + [string]$secretPassword, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerFolders")] + [string]$secretFolderNames, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerDomain")] + [string]$secretDomain = "corp.alkamitech.com", + + [Parameter(Mandatory = $false)] + [Alias("SkipWarmup")] + [switch]$skipAppWarmup, + + [Parameter(Mandatory = $false)] + [Alias("PodGMSAAccount")] + [string]$podGMSAAccountParent + ) + + if ( ( Test-IsAppServer ) -eq $false ) { + $title = "You Are Attempting to Install the ORB App Tier on a Machine Identified as a non-App Tier Server"; + $message = "Do you want to quit?"; + + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Breaks execution." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Continues execution." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + if ( $result -eq 0 ) { + Write-Host "Cancelling installation." + return + } + } + + $logLead = (Get-LogLeadName) + + Write-Output ("$logLead : Configuring Machine as App Tier Server for POD {0}" -f [Environment]::GetEnvironmentVariable("POD", "Machine")) + + if (![String]::IsNullOrEmpty($secretUserName) -and ![String]::IsNullOrEmpty($secretPassword) -and ![String]::IsNullOrEmpty($secretFolderNames) -and ![String]::IsNullOrEmpty($secretDomain)) { + Read-AppTierSecrets $secretUserName $secretPassword $secretFolderNames $secretDomain + } + + if (![String]::IsNullOrEmpty($podGMSAAccountParent)) { + Set-AppTierGMSAAccounts $podGMSAAccountParent + } + + if (($appTierApplications | Where-Object {$_.User.EndsWith("$") -and $_.IsGMSAAccount}).Count -gt 0 -or ((Get-AppTierServices) | Where-Object {$_.User.EndsWith("$") -and $_.IsGMSAAccount}).Count -gt 0) { + Test-AppTierGMSAAccounts + } + + Set-AppTierDefaultWebSite + Set-AppTierFolderAndFilePermissions + New-MachineConfigConnectionString $true + New-MachineConfigConnectionString $false + + Write-Output "$logLead : Running the CreateSymLinks Script" + New-OrbSymLinks + + Write-Output "$logLead : Creating AppPools, Services, Configs, etc" + + New-AppTierWebApplications + New-AppTierWindowsServices + New-AppTierHostFileEntries + New-MachineConfigMachineKeys + + Rename-NewLogConfig + Set-DefaultTLSVersion + + Write-Output "$logLead : Calling Disable-Nag" + Disable-Nag + + Set-RapidFailSettings + + if ($skipAppWarmup) { + return + } + + Write-Output "$logLead : Warming up services..." + Ping-AlkamiServices +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Install-ORBWebServer.ps1 b/Modules/Alkami.DevOps.Installation/Public/Install-ORBWebServer.ps1 new file mode 100644 index 0000000..8ea8255 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Install-ORBWebServer.ps1 @@ -0,0 +1,120 @@ +function Install-ORBWebServer { +<# +.SYNOPSIS + Installs an ORB Web Server. +#> + + # Todo: Set parameter groups and verification + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("ClientSiteUrl")] + [string]$clientUrl, + + [Parameter(Mandatory = $false)] + [Alias("AdminSiteUrl")] + [string]$adminUrl, + + [Parameter(Mandatory = $false)] + [Alias("IpstsSiteUrl")] + [string]$ipstsUrl, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerUserName")] + [string]$secretUserName, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerPassword")] + [string]$secretPassword, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerFolders")] + [string]$secretFolderNames, + + [Parameter(Mandatory = $false)] + [Alias("SecretServerDomain")] + [string]$secretDomain = "corp.alkamitech.com", + + [Parameter(Mandatory = $false)] + [Alias("AppTierVIP")] + [string]$appTierVipPrefix, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("SkipWarmup")] + [switch]$skipSiteWarmup, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineAdminAppPools")] + [switch]$doCombineAdminAppPools, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineClientAppPools")] + [switch]$doCombineClientAppPools, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineIPSTSAppPools")] + [switch]$doCombineIPSTSAppPools, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineAdminWebSites")] + [switch]$doCombineAdminWebSites, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineClientWebSites")] + [switch]$doCombineClientWebSites, + + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] + [Alias("CombineIPSTSWebSites")] + [switch]$doCombineIPSTSWebSites + ) + + if (Test-IsAppServer) { + $title = "You Are Attempting to Install the ORB Web Tier on a Machine Identified as an App Tier Server" + $message = "Do you want to quit?" + + $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Breaks execution." + $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Continues execution." + $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) + $result = $host.ui.PromptForChoice($title, $message, $options, 0) + + switch ($result) { + 0 { return } + } + } + + $logLead = (Get-LogLeadName); + + Write-Output ("$logLead : Configuring Machine as Web Tier Server for POD {0}" -f [Environment]::GetEnvironmentVariable("POD", "Machine")); + + if (![String]::IsNullOrEmpty($secretUserName) -and ![String]::IsNullOrEmpty($secretPassword) -and ![String]::IsNullOrEmpty($secretFolderNames) -and ![String]::IsNullOrEmpty($secretDomain)) { + Read-WebTierSecrets $secretUserName $secretPassword $secretFolderNames $secretDomain; + } + + Set-WebTierDefaultWebSite; + Set-WebTierFolderAndFilePermissions; + Set-ServerResponseHeaders; + Set-ServerMIMETypes; + New-WebTierMachineConfigAppSettings; + New-MachineConfigMachineKeys; + + Write-Output "$logLead : Running New-OrbSymLinks" + New-OrbSymLinks + + New-WebTierWebSites $clientUrl $adminUrl $ipstsUrl -CombineAdminAppPools:$doCombineAdminAppPools.IsPresent -CombineClientAppPools:$doCombineClientAppPools.IsPresent -CombineIPSTSAppPools:$doCombineIPSTSAppPools.IsPresent -CombineAdminWebSites:$doCombineAdminWebSites.IsPresent -CombineClientWebSites:$doCombineClientWebSites.IsPresent -CombineIPSTSWebSites:$doCombineIPSTSWebSites.IsPresent; + + if (!([String]::IsNullOrEmpty($appTierVipPrefix))) { + New-VIPsHostFileEntries $appTierVipPrefix; + } + + Rename-NewLogConfig; + + Set-RapidFailSettings; + + if ($skipSiteWarmup) { + return + } + + Start-IISAndServices; + Write-Output "$logLead : Warming up sites..."; + Ping-AlkamiWebSites; +} diff --git a/Modules/Alkami.DevOps.Installation/Public/New-DummyPackageInstallationData.ps1 b/Modules/Alkami.DevOps.Installation/Public/New-DummyPackageInstallationData.ps1 new file mode 100644 index 0000000..43fdb7b --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/New-DummyPackageInstallationData.ps1 @@ -0,0 +1,127 @@ +function New-DummyPackageInstallationData { + <# + .SYNOPSIS + Create a new PackageMetadata object for unit tests. Utilized by Classify-Packages tests. + .PARAMETER PackageName + Name of the package being created. + .PARAMETER PackageVersion + Version of the package being created. + .PARAMETER IsWebOnly + Indicates that the package is only installed on Web servers. + .PARAMETER IsAppOnly + Indicates that the package is only installed on App servers. + .PARAMETER IsMicOnly + Indicates that the package is only installed on Mic servers. + .PARAMETER IsFullScale + Indicates that the package is only installed everywhere. + .PARAMETER IsHotFix + Indicates that the package is a Hotfix package. + + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true, ParameterSetName = "FullScale")] + [Parameter(Mandatory = $true, ParameterSetName = "Web")] + [Parameter(Mandatory = $true, ParameterSetName = "App")] + [Parameter(Mandatory = $true, ParameterSetName = "Mic")] + [string]$PackageName, + [Parameter(Mandatory = $true, ParameterSetName = "FullScale")] + [Parameter(Mandatory = $true, ParameterSetName = "Web")] + [Parameter(Mandatory = $true, ParameterSetName = "App")] + [Parameter(Mandatory = $true, ParameterSetName = "Mic")] + [string]$PackageVersion, + [Parameter(Mandatory = $true, ParameterSetName = "Web")] + [switch]$IsWebOnly, + [Parameter(Mandatory = $true, ParameterSetName = "App")] + [switch]$IsAppOnly, + [Parameter(Mandatory = $true, ParameterSetName = "Mic")] + [switch]$IsMicOnly, + [Parameter(Mandatory = $true, ParameterSetName = "FullScale")] + [switch]$IsFullScale, + [Parameter(Mandatory = $false)] + [switch]$IsHotfix, + [bool]$IsValid = $true + ) + + $tags = @() + + $tier = 2 + $installToWeb = $false + $installToApp = $false + $installToMic = $false + + if ($IsWebOnly) { + $tags = @( + "ORB", + "Widget" + ) + $installToWeb = $true + } + if ($IsAppOnly) { + $tags = @( + "ORB", + "Generic", + "SSO", + "Provider" + ) + $installToApp = $true + } + if ($IsFullScale) { + $tags = @( + "Service", + "Infrastructure" + ) + $installToWeb = $true + $installToApp = $true + $installToMic = $true + $tier = 0 + } + if ($IsMicOnly) { + $tags = @( + "Service", + "Provider" + ) + $installToMic = $true + } + + $objectTemplate = @{ + Tags = $tags + Name = $PackageName + IsReliableService = $false + Version = $PackageVersion + HasInfrastructureMigration = $false + Upgrade = $false + IsService = $IsService -or $IsFullScale + InstallToAppTier = $installToApp + InstallToMicTier = $installToMic + InstallToWebTier = $installToWeb + IsMicroservice = $IsService -or $IsFullScale + IsFullScaleMicroservice = $IsFullScale + ForceSameVersion = $true + PreventRollback = $false + IsDbms = $false + Feed = @{ + Name = "imaginaryfeed" + Source = "https://imaginaryfeed/nuget/feed" + IsDefault = $false + Priority = "0" + Disabled = $false + IsSDK = $false + } + IsInstaller = $false + IsInfrastructure = $IsFullScale + StartMode = $null + NewRelicAppName = $null + IsSDK = $false + IsValid = $IsValid + IsHotfix = $IsHotfix + InstallToWeb = $installToWeb + InstallToFab = $false + InstallToMic = $installToMic + InstallToApp = $installToApp + HasAlkamiManifest = $true + Tier = $tier + } + + return (New-Object -TypeName PSObject -Prop $objectTemplate) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/New-PackageMetadataObject.ps1 b/Modules/Alkami.DevOps.Installation/Public/New-PackageMetadataObject.ps1 new file mode 100644 index 0000000..147e1d8 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/New-PackageMetadataObject.ps1 @@ -0,0 +1,83 @@ +function New-PackageMetadataObject { + <# + .SYNOPSIS + Creates a (mostly) empty metadata object. Utilized by Classify-Packages. + + .PARAMETER ServerFilter + Limits the deployment to a specific list of servers. + + .PARAMETER ServersString + List of servers. + #> + + [CmdletBinding()] + param( + $ServerFilter, + $ServersString + ) + + $packageMetadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @() + MicPackagesToInstall = @() + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + MicPackagesToUninstall = @() + + AwsSettings = @{ } + + HasWebInstalls = $false + HasAppInstalls = $false + HasMicInstalls = $false + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + ServerFilter = @() + ServerFilterRaw = $ServerFilter + OriginalServerList = @() + OriginalServerListRaw = $ServersString + + Servers = @() + ServersToQuery = @() + + WebServers = @() + AppServers = @() + MicServers = @() + FabServers = @() + HasWebServers = $false + HasAppServers = $false + HasMicServers = $false + HasFabServers = $false + SelectedFabServer = "" + + HasBadPackages = $false + BadWebPackagesToUninstall = @() + BadAppPackagesToUninstall = @() + BadMicPackagesToUninstall = @() + BadFabPackagesToUninstall = @() + + EnvironmentLabel = "" + EnvironmentName = "" + EnvironmentNameSafeDesignation = "" + EnvironmentHosting = "" + EnvironmentType = "" + InstalledOrbVersion = "" + IsDisasterRecovery = "" + ForceReinstallPackages = "" + InstallToAppsAndMics = "" + PackageToVersions = @{ } + IsRollingDeploy = $false + DisableMicroserviceNewRelic = $false + + PackageToServers = @{} + MigrationOnlyPackages = @() + + IsEclairInstalledOnAllHosts = $false + EclairInstallData = @() + + } + + return $packageMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/New-WebTierWebSites.ps1 b/Modules/Alkami.DevOps.Installation/Public/New-WebTierWebSites.ps1 new file mode 100644 index 0000000..fa38924 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/New-WebTierWebSites.ps1 @@ -0,0 +1,91 @@ +function New-WebTierWebSites { +<# +.SYNOPSIS + Upserts Web Tier Websites. +#> + + [CmdletBinding()] + Param( + [string]$clientUrl, + [string]$adminUrl, + [string]$ipstsUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineAdminAppPools")] + [bool]$doCombineAdminAppPools, + + [Parameter(Mandatory = $false)] + [Alias("CombineClientAppPools")] + [bool]$doCombineClientAppPools, + + [Parameter(Mandatory = $false)] + [Alias("CombineIPSTSAppPools")] + [bool]$doCombineIPSTSAppPools, + + [Parameter(Mandatory = $false)] + [Alias("CombineAdminWebSites")] + [bool]$doCombineAdminWebSites, + + [Parameter(Mandatory = $false)] + [Alias("CombineClientWebSites")] + [bool]$doCombineClientWebSites, + + [Parameter(Mandatory = $false)] + [Alias("CombineIPSTSWebSites")] + [bool]$doCombineIPSTSWebSites + ) + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : CombineAdminAppPools is {0}" -f $doCombineAdminAppPools) + Write-Verbose ("$logLead : CombineClientAppPools is {0}" -f $doCombineClientAppPools) + Write-Verbose ("$logLead : CombineIPSTSAppPools is {0}" -f $doCombineIPSTSAppPools) + + Write-Verbose ("$logLead : CombineAdminWebSites is {0}" -f $doCombineAdminWebSites) + Write-Verbose ("$logLead : CombineClientWebSites is {0}" -f $doCombineClientWebSites) + Write-Verbose ("$logLead : CombineIPSTSWebSites is {0}" -f $doCombineIPSTSWebSites) + + if (!([String]::IsNullOrEmpty($clientUrl))) { + if ($doCombineClientWebSites) { + New-ClientWebBinding $clientUrl -CombineClientAppPools $doCombineClientAppPools + # New-WebTierWebApplications "WebClient" + } + else { + New-ClientWebSite $clientUrl -CombineClientAppPools $doCombineClientAppPools + # New-WebTierWebApplications $clientUrl + } + } + + if (!([String]::IsNullOrEmpty($adminUrl))) { + if ($doCombineAdminWebSites) { + New-AdminWebBinding $adminUrl -CombineAdminAppPools $doCombineAdminAppPools + } + else { + New-AdminWebSite $adminUrl -CombineAdminAppPools $doCombineAdminAppPools + } + } + + if (!([String]::IsNullOrEmpty($ipstsUrl))) { + if ($doCombineIPSTSWebSites) { + New-IPSTSWebBinding $ipstsUrl -CombineIPSTSAppPools $doCombineIPSTSAppPools + } + else { + New-IPSTSWebSite $ipstsUrl -CombineIPSTSAppPools $doCombineIPSTSAppPools + } + } + + if ([String]::IsNullOrEmpty($ipstsUrl) -and + [String]::IsNullOrEmpty($adminUrl) -and + [String]::IsNullOrEmpty($clientUrl)) { + $clients = Get-ClientWebSiteInformationFromDatabase + + foreach ($client in $clients) { + New-ClientWebBinding $client.Client -CombineClientAppPools $doCombineClientAppPools + # New-WebTierWebApplications $client.Client + New-AdminWebBinding $client.Admin -CombineAdminAppPools $doCombineAdminAppPools + New-IPSTSWebBinding $client.IPSTS -CombineIPSTSAppPools $doCombineIPSTSAppPools + } + } +} + +Set-Alias -name Create-WebTierWebSites -value New-WebTierWebSites; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Read-AppTierSecrets.ps1 b/Modules/Alkami.DevOps.Installation/Public/Read-AppTierSecrets.ps1 new file mode 100644 index 0000000..8a89333 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Read-AppTierSecrets.ps1 @@ -0,0 +1,62 @@ +function Read-AppTierSecrets { +<# +.SYNOPSIS + Reads App Tier Secrets. +#> + + [CmdletBinding()] + Param( + [string]$secretUserName, + [string]$secretPassword, + [string]$secretFolder, + [string]$secretDomain + ) + + $logLead = (Get-LogLeadName); + $hasCerts = $false + + # Create a temporary download folder for certificates + $randomFolderName = [System.IO.Path]::GetRandomFileName().Split('.') | Select-Object -First 1 + $downloadFolder = Join-Path $PSScriptRoot $randomFolderName + + if (!([System.IO.Directory]::Exists($downloadFolder))) { + Write-Verbose ("$logLead : Creating temporary download folder {0}" -f $downloadFolder) + New-Item $downloadFolder -ItemType Directory -Force | Out-Null + } + + # Pull Secrets + Write-Output ("$logLead : Getting AppServer Secrets for Folder {0} using user {1}" -f $secretFolder, $secretUserName) + $secrets = Get-SecretsForPod $secretUserName $secretPassword $secretDomain $secretFolder + + $savedCertificates = @() + + # Have to explicitly call GetEnumerator because of the way PS handles Dictionaries to HashTables + foreach ($secret in $secrets.GetEnumerator()) { + [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null + + if ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::Certificate) { + $cert = [Alkami.Ops.SecretServer.Model.Certificate]$secret.Value + Save-CertificatesToDisk $cert ([ref]$savedCertificates) $downloadFolder + $hasCerts = $true + } + elseif ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::User) { + Set-ServiceAccountValue ([Alkami.Ops.SecretServer.Model.User]$secret.Value) + } + elseif ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::ConnectionString -and $masterConnectionString -eq "REPLACEME") { + $secretConnectionString = ([Alkami.Ops.SecretServer.Model.ConnectionString]$secret.Value).RawConnectionString + Write-Output ("$logLead : Setting master connection string to {0}" -f $secretConnectionString) + $global:masterConnectionString = $secretConnectionString + } + } + + if ($hasCerts) { + Read-AppTierCertificates $downloadFolder $savedCertificates + } + + if (Test-Path $downloadFolder) { + Write-Verbose ("$logLead : Removing temporary download folder {0}" -f $downloadFolder) + Remove-Item $downloadFolder -Recurse -Force + } +} + +Set-Alias -name Load-AppTierSecrets -value Read-AppTierSecrets; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Read-WebTierSecrets.ps1 b/Modules/Alkami.DevOps.Installation/Public/Read-WebTierSecrets.ps1 new file mode 100644 index 0000000..457c7f0 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Read-WebTierSecrets.ps1 @@ -0,0 +1,72 @@ +function Read-WebTierSecrets { +<# +.SYNOPSIS + Reads Web Tier Secrets. +#> + + [CmdletBinding()] + Param( + [string]$secretUserName, + [string]$secretPassword, + [string]$secretFolder, + [string]$secretDomain + ) + + $logLead = (Get-LogLeadName); + $hasCerts = $false + + # Create a temporary download folder for certificates + $randomFolderName = [System.IO.Path]::GetRandomFileName().Split('.') | Select-Object -First 1 + $downloadFolder = Join-Path $PSScriptRoot $randomFolderName + + if (!([System.IO.Directory]::Exists($downloadFolder))) { + Write-Verbose ("$logLead : Creating temporary download folder {0}" -f $downloadFolder) + New-Item $downloadFolder -ItemType Directory -Force | Out-Null + } + + # Pull Secrets + Write-Output ("$logLead : Getting WebServer Secrets for Folder {0} using user {1}" -f $secretFolder, $secretUserName) + $secrets = Get-SecretsForPod $secretUserName $secretPassword $secretDomain $secretFolder + + $savedCertificates = @() + + # Have to explicitly call GetEnumerator because of the way PS handles Dictionaries to HashTables + foreach ($secret in $secrets.GetEnumerator()) { + [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null + + if ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::Certificate) { + $cert = [Alkami.Ops.SecretServer.Model.Certificate]$secret.Value + Save-CertificatesToDisk $cert ([ref]$savedCertificates) $downloadFolder + $hasCerts = $true + } + elseif ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::User) { + if ($secret.Value.SecretName -like "*localreport*") { + Write-Output ("$logLead : Setting ReportServer local reports user to {0}" -f $secret.Value.UserName) + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServerUserName"}).Value = $secret.Value.UserName + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServerPassword"}).Value = $secret.Value.Password + } + elseif ($secret.Value.SecretName -like "*adminreport*") { + Write-Output ("$logLead : Setting ReportServer admin reports user to {0}" -f $secret.Value.UserName) + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportUserName"}).Value = $secret.Value.UserName + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportPassword"}).Value = $secret.Value.Password + } + } + elseif ($secret.Value.SecretType -eq [Alkami.Ops.SecretServer.Enum.SecretType]::ConnectionString -and $masterConnectionString -eq "REPLACEME") { + $secretConnectionString = ([Alkami.Ops.SecretServer.Model.ConnectionString]$secret.Value).ConnectionStringBuilder + Write-Output ("$logLead : Setting ReportServer URL to server {0}" -f $secretConnectionString.DataSource) + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServer"}).Value = ("http://" + $secretConnectionString.DataSource) + ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServerUrl"}).Value = ("http://" + $secretConnectionString.DataSource + "/Pages/ReportViewer.aspx") + } + } + + if ($hasCerts) { + Read-WebTierCertificates $downloadFolder $savedCertificates + } + + if (Test-Path $downloadFolder) { + Write-Verbose ("$logLead : Removing temporary download folder {0}" -f $downloadFolder) + Remove-Item $downloadFolder -Recurse -Force + } +} + +Set-Alias -name Load-WebTierSecrets -value Read-WebTierSecrets; \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Remove-OutdatedNewHotfixPackages.ps1 b/Modules/Alkami.DevOps.Installation/Public/Remove-OutdatedNewHotfixPackages.ps1 new file mode 100644 index 0000000..3f4ea64 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Remove-OutdatedNewHotfixPackages.ps1 @@ -0,0 +1,113 @@ +function Remove-OutdatedNewHotfixPackages{ + <# + .SYNOPSIS + Removes hotfix packages supplied by the user if they are outdated given the existing or new orb version. + + .PARAMETER DependencyReleaseValue + The value of the orb release being classified. Only populated for Orb installs. + + .PARAMETER PackageMetadata + Packages object to populate. + #> + + [CmdletBinding()] + [OutputType([PSObject])] + Param( + [Parameter(Mandatory = $false)] + [string]$DependencyReleaseValue = "", + + [Parameter(Mandatory = $true)] + [PSObject] $PackageMetadata + ) + $loglead = Get-LogleadName + + if ($PackageMetadata.ForceReinstallPackages) { + if (Test-StringIsNullOrWhiteSpace -Value $DependencyReleaseValue) { + Write-Host "##teamcity[message text='DependencyReleaseValue is NullOrWhiteSpace on an ORB deploy!' status='WARNING']" + } + + # matches + # release dot 4digits dot at-least-one-digit dot at-least-one-digit dot at-least-one-digit dot zip + # parens around (\d{4}\.\d+\.\d+\.\d+) "captures" that string that looks like YYYY.N.N.N or 2022.3.0.9 + # which is our Release Version Number, or OrbVersion, matching what Get-OrbVersion would return + $regexOrbReleaseFile = "release\.(\d{4}\.\d+\.\d+\.\d+)\.zip" + if ($DependencyReleaseValue -match $regexOrbReleaseFile) { + $releaseVersion = $Matches[1] + Write-Host "$loglead : Valid release version found in dependency parameters" + $PackageMetadata.InstalledOrbVersion = $releaseVersion + $releaseVersionIsCanonical = $true + } else { + Write-Host "##teamcity[message text='DependencyReleaseValue does not match release name format. Fallback to Get-OrbVersion from remote host' status='WARNING']" + } + } else { + Write-Host "$loglead : This is a package deploy, so we're using Get-OrbVersion from a remote host to determine what to do with hotfixes." + } + + $firstWebORBServer = $PackageMetadata.WebServers | Select-Object -First 1 + $firstAppORBServer = $PackageMetadata.AppServers | Select-Object -First 1 + $firstMicORBServer = $PackageMetadata.MicServers | Select-Object -First 1 + + $orbServersToCheck = @($firstWebORBServer, $firstAppORBServer, $firstMicORBServer) + foreach ($server in $orbServersToCheck) { + if ( -NOT (Test-StringIsNullOrWhiteSpace -Value $server)) { + if (-NOT $releaseVersionIsCanonical) { + $orbVersion = Get-OrbVersion -ComputerName $server + $PackageMetadata.InstalledOrbVersion = $orbVersion + Write-Host "$logLead : Found orb version $orbVersion." + # Found an orb version, don't do this more than necessary. + break; + } + } + } + + $versionIsLessThanTarget = -1 + $versionIsSameAsTarget = 0 + $versionIsGreaterThanTarget = 1 + + $webHotfixPackagesToRemove = @() + $appHotfixPackagesToRemove = @() + + # Check incoming Web packages. + foreach($package in $PackageMetadata.WebPackagesToInstall) + { + if($package.IsHotfix){ + $compareSemverResult = Compare-SemVer -Version1 $package.HotfixFixedInOrbVersion -Version2 $PackageMetadata.InstalledOrbVersion + + switch ($compareSemverResult) { + $versionIsLessThanTarget { $webHotfixPackagesToRemove += $package.Name } + $versionIsSameAsTarget { $webHotfixPackagesToRemove += $package.Name } + $versionIsGreaterThanTarget { <# Do nothing, this will get reinstalled if it should be reinstalled at this point #> } + } + } + } + + # This is a list of Package NAMES, not packages. + foreach($package in $webHotfixPackagesToRemove){ + Write-Host "##teamcity[message text='Found a hotfix in the supplied WEB install list which is not valid with this version of Orb. It will not be installed: $package .' status='WARNING']" + + [array]$PackageMetadata.WebPackagesToInstall = ([array]$PackageMetadata.WebPackagesToInstall).Where( { $_.Name -ne $package }) + } + + # Check incoming App packages. + foreach($package in $PackageMetadata.AppPackagesToInstall) + { + if($package.IsHotfix){ + $compareSemverResult = Compare-SemVer -Version1 $package.HotfixFixedInOrbVersion -Version2 $PackageMetadata.InstalledOrbVersion + + switch ($compareSemverResult) { + $versionIsLessThanTarget { $appHotfixPackagesToRemove += $package.Name } + $versionIsSameAsTarget { $appHotfixPackagesToRemove += $package.Name } + $versionIsGreaterThanTarget { <# Do nothing, this will get reinstalled if it should be reinstalled at this point #> } + } + } + } + + # This is a list of Package NAMES, not packages. + foreach($package in $appHotfixPackagesToRemove){ + Write-Host "##teamcity[message text='Found a hotfix in the supplied APP install list which is not valid with this version of Orb. It will not be installed: $package .' status='WARNING']" + + [array]$PackageMetadata.AppPackagesToInstall = ([array]$PackageMetadata.AppPackagesToInstall).Where( { $_.Name -ne $package }) + } + + return $PackageMetadata +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.ps1 new file mode 100644 index 0000000..41ece1b --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.ps1 @@ -0,0 +1,101 @@ +function Set-InfrastructureConfiguration { + +<# +.SYNOPSIS + Writes the expected NewRelic Infrastructure configuration file using environment specific parameters + +.DESCRIPTION + Writes the expected NewRelic Infrastructure configuration file using environment specific parameters. Sets + custom attributes for Pod and ServerRole based on user input. Calculated license key is looked up from + function Get-NewRelicAccountDetails using an environment key + +.PARAMETER ServerRole + [string] The literal value to use for the ServerRole custom attribute + +.PARAMETER Pod + [string] The literal value to use for the Pod custom attribute + +.PARAMETER EnvironmentKey + [string] The environment key, such as "AWS Production Pod 5", which is used to determine the appropriate license key for NewRelic + +.PARAMETER ConfigurationFilePath + [string] The filepath to the NewRelic infrastructure configuration file. Defaults to ""C:\Program Files\New Relic\newrelic-infra\newrelic-infra.yml" + +.INPUTS + None + +.OUTPUTS + None + +.EXAMPLE +Set-InfrastructureConfiguration -ServerRole "BitcoinMiner" -Pod "99" -EnvironmentKey "AWS Dev Team 99" -Verbose + +VERBOSE: [Set-InfrastructureConfiguration] : Using ServerRole: [BitcoinMiner] +VERBOSE: [Set-InfrastructureConfiguration] : Using Pod: [99] +VERBOSE: [Set-InfrastructureConfiguration] : ConfigurationFilePath: [C:\Program Files\New Relic\newrelic-infra\newrelic-infra.yml] +VERBOSE: [Get-NewRelicAccountDetails] : No specific account found for 'AWS Dev Team 99'; looking for default. +VERBOSE: [Get-NewRelicAccountDetails] : Returning entry with EnvironmentRegex '((? + + #TODO replace with AWS logic and pull tags instead once we move to AWS + #TODO this function name is terrible + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$ServerRole, + + [Parameter(Mandatory = $true)] + [string]$Pod, + + [Parameter(Mandatory = $true)] + [string]$EnvironmentKey + ) + + $logLead = Get-LogLeadName + + Import-AWSModule + + $ConfigurationFilePath = Get-NewRelicYamlPath + # Returning/stopping exactly where the error is + if (Test-StringIsNullOrWhitespace $ConfigurationFilePath) { return } + + Write-Verbose "$logLead : Using ServerRole: [$ServerRole]" + Write-Verbose "$logLead : Using Pod: [$Pod]" + Write-Verbose "$logLead : ConfigurationFilePath: [$ConfigurationFilePath]" + + $licenseKey = (Get-NewRelicAccountDetails $environmentKey).LicenseKey + Write-Verbose "$logLead : Using license key: $licenseKey" + + Set-Content $ConfigurationFilePath @" +license_key: $licenseKey +disable_cloud_instance_id: true +custom_attributes: + ServerRole: $serverRole + Pod: '$Pod' +"@ + + if (Test-IsAWS) { + # We already know the instance exists, so we want to see if it has a tag for alk:overflow + # It can only return if all three are true, so there will never be two record-sets returned in the response + # If the instance has this tag, we will get some set of results back in the response, so we should always set the custom_attribute.Overflow to true + # If the instance does not have this tag, we will get no results back in the response, so we should always set the custom_attribute.Overflow to false + # We should always set the flag (even if it is to false) to ensure that we are properly reporting server intent to NR + $hostname = $env:COMPUTERNAME.ToLower() + $Ec2TagFilter = @{Name = "tag:alk:hostname"; Values = "$hostname" }, + @{Name = "resource-type"; Values = "instance" }, + @{Name = "tag:alk:overflow"; Values = "true" } + $scriptBlock = { Get-EC2Tag -Filter $Ec2TagFilter } + $instanceOverflowTag = Invoke-CommandWithRetry -Arguments @($hostname) -ScriptBlock $scriptBlock -MaxRetries 3 -SecondsDelay 1 + + if (!(Test-IsCollectionNullOrEmpty $instanceOverflowTag)) { + Add-OverflowCustomAttribute -IsOverflow $true + } else { + Add-OverflowCustomAttribute -IsOverflow $false + } + } + + Write-Host "$logLead : NewRelic Infrastructure configuration saved to $ConfigurationFilePath" +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.tests.ps1 new file mode 100644 index 0000000..8f94cc1 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-InfrastructureConfiguration.tests.ps1 @@ -0,0 +1,138 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-InfrastructureConfiguration" { + + Context "Function Tests" { + + + It "Uses the Supplied ServerRole in the Output File" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "abcdefg123456"; + } + } + + Set-InfrastructureConfiguration -ServerRole "BitcoinMiner" -Pod "99" -EnvironmentKey "abcdefg123456" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Match " ServerRole: BitcoinMiner" + } + + It "Uses the Supplied Pod in the Output File" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "abcdefg123456"; + } + } + + Set-InfrastructureConfiguration -ServerRole "Web" -Pod "98.70" -EnvironmentKey "abcdefg123456" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Match " Pod: '98\.70'" + } + + It "Does not use the Supplied Pod in the Output File" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "abcdefg123456"; + } + } + + Set-InfrastructureConfiguration -ServerRole "Web" -Pod "98670" -EnvironmentKey "abcdefg123456" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Not -Match " Pod: '98\.70'" + } + + It "Does not use the Supplied Pod in the Output File with no single quotes" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "abcdefg123456"; + } + } + + Set-InfrastructureConfiguration -ServerRole "Web" -Pod "98.70" -EnvironmentKey "abcdefg123456" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Not -Match " Pod: 98\.70" + } + + It "Uses the Supplied LicenseKey in the Output File" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "hunter2"; + } + } + + Set-InfrastructureConfiguration -ServerRole "Web" -Pod "187" -EnvironmentKey "abcdefg123456" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Match "^license_key: hunter2" + } + + It "Uses 4 Preceeding Spaces for All Indented Lines" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "fakelicense"; + } + } + + Set-InfrastructureConfiguration -ServerRole "FakeRole" -Pod "FakePod" -EnvironmentKey "abcdefg123456" ` + + $leadingWhiteSpaceContent = Get-Content $testConfiguration | Where-Object { $_ -match "^\s" } + + foreach ($contentLine in $leadingWhiteSpaceContent) { + + $contentLine | Should -Match "^ " -Because "YAML Indented Lines Must Start with 4 Spaces, Not Tabs" + $contentLine | Should -Not -Match "^\t" -Because "YAML Indented Lines Must Start with 4 Spaces, Not Tabs" + } + } + + It "Includes the Disable Cloud Instance ID Attribute" { + + $testConfiguration = "TestDrive:\newrelic-infra.yml" + Mock -CommandName Get-NewRelicYamlPath -MockWith { "TestDrive:\newrelic-infra.yml" } + Mock -CommandName Get-NewRelicAccountDetails -ModuleName $moduleForMock { + + return New-Object PSObject -Property @{ + LicenseKey = "fakelicense2"; + } + } + + Set-InfrastructureConfiguration -ServerRole "FakeRole2" -Pod "FakePod2" -EnvironmentKey "abcdefg1234562" ` + + $content = Get-Content $testConfiguration -Raw + $content | Should -Match "disable_cloud_instance_id: true" -Because "NewRelic Infrastructure May Use the Instance ID instead of Hostname if Not Present" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.Tests.ps1 new file mode 100644 index 0000000..af1de94 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.Tests.ps1 @@ -0,0 +1,290 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-NewRelicConfigurationValues + +Describe "Set-NewRelicConfigurationValues" { + + $global:modifiedXml = $null + + Mock -ModuleName $moduleForMock Save-XmlFile { + + Param( + [string]$path, + [xml]$xml + ) + + $global:modifiedXml = $xml + } + Mock -ModuleName $moduleForMock Write-Host + Mock -ModuleName $moduleForMock Write-Warning + + # XML With Wrong Values for both Log Levels and Slow SQL + [xml]$global:nrXmlWrongValues = @' + + + + + + + + + + +'@ + + Context "When the Values are Not Alkami Defaults" { + + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { + return $global:nrXmlWrongValues + } + + Set-NewRelicConfigurationValues + + It "Sets the Log Level to Error" { + + Assert-MockCalled Read-XmlFile -ModuleName $moduleForMock -times 1 + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + + It "Sets explainsEnabled to False" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + + It "Sets SlowSql to False" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + + It "Sets applicationLogging to False" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + + It "Sets applicationLoggingforwarding to False" { + + $modifiedXml.OuterXml.ToString() | Should -Match ' + # + # + # + # + #'@ + + $global:modifiedXml = $null + + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlWrongValues } + + Set-NewRelicConfigurationValues -LogLevel "FATAL" + + It "Sets the Log Level to the Provided Value" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + } + + Context "When the transactionTracer Node is Missing" { + + $global:modifiedXml = $null + + # XML With Missing Slow SQL Node + [xml]$global:nrXmlNoSlowSql = @' + + + + + + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoSlowSql } + + Set-NewRelicConfigurationValues + + It "Adds the Missing transactionTracer Node" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + } + + + Context "When the applicationLogging Node is Missing" { + + $global:modifiedXml = $null + + # XML With Missing Slow SQL Node + [xml]$nrXmlNoSlowSql = @' + + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoSlowSql } + + Set-NewRelicConfigurationValues + + It "Adds the Missing applicationLogging Node" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + } + + Context "When the applicationLoggingforwarding Node is Missing" { + + $global:modifiedXml = $null + + # XML With Missing Slow SQL Node + [xml]$nrXmlNoSlowSql = @' + + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoSlowSql } + + Set-NewRelicConfigurationValues + + It "Adds the Missing applicationLoggingforwarding Node" { + + $modifiedXml.OuterXml.ToString() | Should -Match ' + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoSlowSql } + + Set-NewRelicConfigurationValues + + It "Adds the Missing applicationLogginglocalDecorating Node" { + + $modifiedXml.OuterXml.ToString() | Should -Match ' + + + + + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoSlowSql } + + Set-NewRelicConfigurationValues + + It "Adds the Missing SlowSql Node" { + + $modifiedXml.OuterXml.ToString() | Should -Match '' + } + } + + Context "When the NewRelic Configuration File Is Invalid" { + + $tempPath = [System.IO.Path]::GetTempFileName() + + It "Writes a Warning When the File Is Not Found" { + + { (Set-NewRelicConfigurationValues -NewRelicConfigPath $tempPath 3>&1) -match "The New Relic Config File Could Not be Found" + } | Should Be $true + } + + It "Writes a Warning When the Config is Invalid XML" { + + $global:badXml = "Hello World!" + Mock -ModuleName $moduleForMock Read-XmlFile { return $global:badXml } + + { (Set-NewRelicConfigurationValues 3>&1) -match "The New Relic Config File Could Not be Read Or Is Invalid" } | Should Be $true + } + + It "Writes a Warning When the Logging Node Does Not Exist" { + + $global:modifiedXml = $null + + # XML With Missing Slow SQL Node + [xml]$nrXmlNoLogging = @' + + + + +'@ + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrXmlNoLogging } + + { (Set-NewRelicConfigurationValues 3>&1) -match "Log Node was not found" } | Should Be $true + } + } + + Context "When No Changes Are Required" { + + [xml]$nrNoChangesNeeded = @' + + + + + + + + + + +'@ + + It "Does Not Save the Configuration File" { + + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return $nrNoChangesNeeded } + + # Mock Save-XmlFile for the Test + Mock -ModuleName $moduleForMock Save-XmlFile { } + + Set-NewRelicConfigurationValues + Assert-MockCalled Save-XmlFile -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + } +} + +#endregion Set-NewRelicConfigurationValues \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.ps1 new file mode 100644 index 0000000..ea378ab --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicConfigurationValues.ps1 @@ -0,0 +1,215 @@ +function Set-NewRelicConfigurationValues { +<# +.SYNOPSIS + Sets the LogLevel and SlowSql Configuration for the New Relic agent to recommended defaults +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("LogLevel")] + [string]$loggingLevel = "error", + + [Parameter(Mandatory = $false)] + [Alias("NewRelicConfigPath")] + [string]$nrConfigPath = "C:\ProgramData\New Relic\.NET Agent\newrelic.config" + ) + + $logLead = Get-LogLeadName + + try { + Write-Verbose ("$logLead : Looking for New Relic configuration in {0}" -f $nrConfigPath) + + [Xml]$newRelicConfig = Read-XmlFile $nrConfigPath -ErrorAction SilentlyContinue + + if ([String]::IsNullOrEmpty($newRelicConfig)) { + Write-Warning ("$logLead : The New Relic Config File Could Not be Found") + return + } + + } + catch { + Write-Warning "$logLead : The New Relic Config File Could Not be Read Or Is Invalid" + + return + } + + $xmlNameSpace = @{ nr = 'urn:newrelic-config'; } + + Write-Verbose "$logLead : Looking for NewRelic Configuration Node" + + $configurationNode = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + if ($null -eq $configurationNode) { + Write-Warning "$logLead : The New Relic configuration node could not be found. Check the New Relic Agent installation" + + return + } + + Write-Verbose "$logLead : Looking for New Relic Service SSL attr" + $serviceNode = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:service' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + $sslAttr = $null + if ($null -eq $serviceNode) { + Write-Warning "$logLead : Service Node was not found -- this should never be the case. Check the New Relic Agent installation" + } else { + $sslAttr = $serviceNode.Node.Attributes["ssl"] + } + + Write-Verbose "$logLead : Looking for NewRelic Log Level Node" + $loggingNode = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:log/@level' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + Write-Verbose "$logLead : Looking for NewRelic SlowSql Node" + $slowSqlNode = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:slowSql/@enabled' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + Write-Verbose "$logLead : Looking for NewRelic transactionTracer Node" + $transactionTracer = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:transactionTracer/@explainEnabled' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + Write-Verbose "$logLead : Looking for NewRelic applicationLogging Node" + $applicationLogging = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:applicationLogging' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + $applicationLoggingEnabled = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:applicationLogging/@enabled' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + $applicationLoggingForwardingEnabled = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:applicationLogging/nr:forwarding/@enabled' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + $applicationLoggingLocalDecorating = Select-Xml ` + -Xml $newRelicConfig ` + -Xpath '//nr:configuration/nr:applicationLogging/nr:localDecorating/@enabled' ` + -Namespace $xmlNamespace ` + -ErrorAction SilentlyContinue + + $configFileIsDirty = $false + + if ($null -eq $loggingNode) { + Write-Warning "$logLead : Log Node was not found -- this should never be the case. Check the New Relic Agent installation" + + return + } ElseIf ($loggingNode.ToString() -notmatch $loggingLevel) { + Write-Host "$logLead : Updating the New Relic Logging Configuration" + + $configFileIsDirty = $true + + $loggingNode.Node.Value = $loggingLevel; + } + + if ($null -ne $sslAttr) { + Write-Host "$logLead : The Service/Ssl Attr Exists and Will Be Removed" + + $serviceNode.Node.Attributes.Remove($sslAttr) + + $configFileIsDirty = $true + } + + if ($null -eq $slowSqlNode) { + Write-Host "$logLead : The SlowSQL Node Does Not Exist and Will be Created" + + $configFileIsDirty = $true + + $tempDoc = New-Object System.Xml.XmlDocument + $tempDoc.LoadXml('') + + $newSqlNode = $newRelicConfig.ImportNode($tempDoc.DocumentElement, $true) + $newRelicConfig.DocumentElement.AppendChild($newSqlNode) | Out-Null + } Elseif ($slowSqlNode.Node.Value -notmatch "false") { + Write-Host "$logLead : Updating the New Relic SlowSQL Node Configuration" + + $configFileIsDirty = $true + + $slowSqlNode.Node.Value = "false" + } + + if ($null -eq $transactionTracer) { + Write-Host "$logLead : The transactionTracer Node Does Not Exist and Will be Created" + + $configFileIsDirty = $true + + $tempDoc = New-Object System.Xml.XmlDocument + $tempDoc.LoadXml(' ') + + $newSqlNode = $newRelicConfig.ImportNode($tempDoc.DocumentElement, $true) + $newRelicConfig.DocumentElement.AppendChild($newSqlNode) | Out-Null + } ElseIf ($transactionTracer.Node.Value -notmatch "false") { + Write-Host "$logLead : Updating the New Relic transactionTracer Node Configuration" + + $configFileIsDirty = $true + + $transactionTracer.Node.Value = "false" + } + + if ($null -eq $applicationLogging) { + Write-Host "$logLead : The applicationLogging Node Does Not Exist and Will be Created" + + $configFileIsDirty = $true + + $tempDoc = New-Object System.Xml.XmlDocument + $tempDoc.LoadXml('') + + $newSqlNode = $newRelicConfig.ImportNode($tempDoc.DocumentElement, $true) + $newRelicConfig.DocumentElement.AppendChild($newSqlNode) | Out-Null + } else { + + if ($applicationLoggingEnabled.Node.Value -notmatch "false") { + Write-Host "$logLead : Updating the New Relic applicationLogging Node Configuration" + + $configFileIsDirty = $true + + $applicationLoggingEnabled.Node.Value = "false" + } + if ($applicationLoggingForwardingEnabled.Node.Value -notmatch "false") { + Write-Host "$logLead : Updating the New Relic applicationLoggingforwarding Node Configuration" + + $configFileIsDirty = $true + + $applicationLoggingForwardingEnabled.Node.Value = "false" + } + if ($applicationLoggingLocalDecorating.Node.Value -notmatch "false") { + Write-Host "$logLead : Updating the New Relic applicationLogginglocalDecorating Node Configuration" + + $configFileIsDirty = $true + + $applicationLoggingLocalDecorating.Node.Value = "false" + } + } + + + if ($configFileIsDirty) { + Write-Host ("$logLead : Saving the Modified New Relic Configuration File" -f $nrConfigPath) + + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + Save-XMLFile $nrConfigPath $newRelicConfig.OuterXml.Replace('xmlns=""', [String]::Empty) $utfNoBOM + } else { + Write-Host "$logLead : No Changes Required to the New Relic Config" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.ps1 new file mode 100644 index 0000000..13b9b41 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.ps1 @@ -0,0 +1,131 @@ +function Set-NewRelicDeployment { + <# +.SYNOPSIS + Posts a Deployment to a New Relic application via the API for either: + All non-microservices that New Relic knows about in a given Environment + OR + All specified microservices that New Relic knows about in a given Environment. + +.PARAMETER EnvironmentKey + Name of the environment to update. + +.PARAMETER AppVersion + Version of the service that is being deployed. Only valid for legacy services. Providing this and a set of microservices will throw. + +.PARAMETER DeployUser + The user performing the deployment. Optional. + +.PARAMETER Microservices + A hash table of microservices and their versions. + +.EXAMPLE + Set-NewRelicDeployment -EnvironmentKey "AWS CICD CI1" -AppVersion 1.0 -DeployUser "testUser" + + $microservices = @{"Alkami.Microservice.Sample" = "1.0"} + + Set-NewRelicDeployment -EnvironmentKey "AWS CICD CI1" -DeployUser "testUser" -micoserviceNames $microservices + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("Environment")] + [string]$EnvironmentKey, + + [Parameter(Mandatory = $false, + ParameterSetName = "StandardServices")] + [Alias("Version")] + [string]$AppVersion, + + [Parameter(Mandatory = $false)] + [Alias("DeploymentEngineer")] + [string]$DeployUser = "Alkami", + + [Parameter(Mandatory = $false, + ParameterSetName = "Microservices")] + [Alias("SpecifiedMicroservices")] + [hashtable]$Microservices + ) + + $logLead = (Get-LogLeadName) + Write-Host ("$logLead : Pulling list of Applications from NewRelic") + + $apiKey = (Get-NewRelicAccountDetails $EnvironmentKey).APIKey + + $postFailed = @() + + try { + $applications = (Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications" -FilterKey "name" -FilterValue $EnvironmentKey).applications + } catch { + # If an error gets thrown below me on the stack, we should just abort with an error message + Write-Error "$logLead : Error thrown while getting NewRelic objects, can not process deployments. Exiting function with no action performed.`r`n$_" + return + } + + if (![string]::IsNullOrWhiteSpace($EnvironmentKey) -and (Test-IsCollectionNullOrEmpty $applications)) { + Write-Warning "$logLead : No results at all returned from NewRelic for [$EnvironmentKey]. Cannot continue, returning early." + return + } + + $filteredApplications = $applications.Where({$_.name -match "^$($EnvironmentKey)[^.]"}) + if (Test-IsCollectionNullOrEmpty $filteredApplications) { + Write-Warning "$logLead : No results returned from NewRelic that match the filter for [$EnvironmentKey]. Cannot continue, returning early." + return + } + + Write-Host "$logLead : Posting Deployment to NewRelic Applications for Environment [$EnvironmentKey]" + + # If provided with microservice names, loop through and send them to new-relic, if they already exist. + if ($Microservices) { + foreach ($microservice in $Microservices.GetEnumerator()) { + $foundMicroserviceViaFilter = $false + $AppVersion = $microservice.value + $microserviceName = $microservice.key + + $matchedApplications = $filteredApplications.Where({$_.name -match $microserviceName}) + foreach($matchedApplication in $matchedApplications) { + $foundMicroserviceViaFilter = $true + + Write-Verbose "$logLead : Looking to update $($matchedApplication.Name)" + if (Submit-DeploymentToNewRelic -ApiKey $apiKey -ApplicationId $matchedApplication.id -AppVersion $AppVersion -DeployUser $DeployUser -EnvironmentKey $EnvironmentKey) { + Write-Verbose "$logLead : Updated $($matchedApplication.Name)" + } else { + Write-Verbose "$logLead : Failed to update $($matchedApplication.Name)" + $postFailed += $matchedApplication.Name + } + } + if (!$foundMicroserviceViaFilter -and $microserviceName -like "*Alkami.M*") { + Write-Warning "$microserviceName was specified to be updated in New Relic, but New Relic does not know about it for [$EnvironmentKey]. There is likely no entry for [$EnvironmentKey $microserviceName]" + } + } + } else { + if([string]::IsNullOrWhiteSpace($AppVersion)) { + throw "$logLead : Somehow we got to a New Relic API call with no AppVersion value. That shouldn't happen. Please investigate." + } else { + Write-Host "$logLead : Looking for non-service accounts to be updated in NR" + + $matchedApplications = $filteredApplications.Where({$_.name -notlike "*Alkami.M*" }) + + if (Test-IsCollectionNullOrEmpty $matchedApplications) { + Write-Warning "$logLead : No applications found for [$EnvironmentKey] in NewRelic" + } else { + foreach($matchedApplication in $matchedApplications) { + Write-Verbose "$logLead : Looking to update $($matchedApplication.Name)" + if (Submit-DeploymentToNewRelic -ApiKey $apiKey -ApplicationId $matchedApplication.id -AppVersion $AppVersion -DeployUser $DeployUser -EnvironmentKey $EnvironmentKey) { + Write-Verbose "$logLead : Updated $($matchedApplication.Name)" + } else { + Write-Verbose "$logLead : Failed to update $($matchedApplication.Name)" + $postFailed += $matchedApplication.Name + } + } + } + } + } + + if (!(Test-IsCollectionNullOrEmpty $postFailed)) { + foreach($failed in $postFailed) { + Write-Warning "$logLead : Failed to update deployment for [$failed]" + } + throw "$logLead : Not all Post Requests were successful. Review the log output for details" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.tests.ps1 new file mode 100644 index 0000000..d7c5669 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-NewRelicDeployment.tests.ps1 @@ -0,0 +1,194 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-NewRelicDeployment" { +# by using variables for everything, we can eliminate typos and ensure we still match the appropriate/expected actions +#region variable setup + $EnvironmentKey = "Unit Test Environment" + $AlternateEnvironmentKey = "Fake Pod 12" + $TestUser = "TestUser" + $SampleWcfApplicationId = 1234 + $SamplePositiveMicroservice1Id = 5678 + $SamplePositiveMicroservice2Id = 1010 + $SampleNegativeMicroservice1Id = 9876 + $SampleNegativeMicroservice2Id = 8765 + $SampleMicroserviceName1 = "Alkami.Microservice Test 1" + $SampleMicroserviceName2 = "Alkami.Microservice Test 2" + $SampleMicroserviceName3 = "Alkami.Microservice Test 3" + $SampleMicroserviceName4 = "Alkami.Microservice Test 4" +#endregion variable setup + +#region mock setup + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-NewRelicAccountDetails -MockWith { return @{ + EnvironmentRegex = $EnvironmentKey + APIKey = "Unit Test Api Key" + LicenseKey = "Unit Test License Key" + } } + + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -MockWith { Write-warning "$applicationVersion"; return $true } + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -ParameterFilter { $ApplicationId -eq $SampleWcfApplicationId } -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice1Id } -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice2Id } -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice1Id } -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice2Id } -MockWith { return $false } + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -ParameterFilter { $message.IndexOf("$SampleMicroserviceName3 was specified to be updated in New Relic") -gt -1 } -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-NewRelicObjects -MockWith { + return @{ applications = @( + @{ + id = $SampleWcfApplicationId; + name = "$EnvironmentKey Sample WCF Application"; + }, + @{ + id = $SamplePositiveMicroservice1Id; + name = "$EnvironmentKey $SampleMicroserviceName1"; + }, + @{ + id = $SamplePositiveMicroservice2Id; + name = "$EnvironmentKey $SampleMicroserviceName2"; + }, + @{ + id = $SampleNegativeMicroservice1Id; + name = "$AlternateEnvironmentKey $SampleMicroserviceName3"; + }, + @{ + id = $SampleNegativeMicroservice2Id; + name = "$AlternateEnvironmentKey.1 $SampleMicroserviceName4"; + } + )}; + } +#endregion mock setup + + Context "When Microservices Are Not Provided As A Parameter, Post For Services" { + Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -AppVersion "1.1" -DeployUser $TestUser + + It "Posts to non-MS Which Do Exist In New Relic" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleWcfApplicationId } + } + + It "Does Not Post to MS Which Do Exist In New Relic" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 0 -Scope Context -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice1Id } + } + + # We already asserted above that it posts to the one we want, now ensure it only posted the expected amount of times + It "Does Not Post An Update To Additional Services Which Do Exist In New Relic" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context + } + } + + Context "When Making the Application Query" { + It "Throws if AppVersion is not supplied" { + { Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -DeployUser $TestUser } | Should -throw + } + + It "Does not throw when required parameters are provided" { + { + Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -AppVersion "1.1" -DeployUser $TestUser + } | Should -Not -Throw + } + + It "Throws when too many parameters are provided for input" { + $microservices = @{ + $SampleMicroserviceName1 = "1.1" + $SampleMicroserviceName2 = "1.1" + } + { + Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -AppVersion "1.1" -DeployUser $TestUser -microservices $microservices + } | Should -Throw + } + } + + Context "When 2 Microservices Are Provided As A Parameter and all belong to the environment" { + $microservices = @{ + $SampleMicroserviceName1 = "1.1" + $SampleMicroserviceName2 = "1.1" + } + + It "Does not throw when MS are provided for input" { + { + Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -DeployUser $TestUser -microservices $microservices + } | Should -Not -Throw + } + + It "Does Not Report a Warning To Provided Microservices Which Exist In New Relic - 1" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice1Id } + } + + It "Does Not Report a Warning To Provided Microservices Which Exist In New Relic - 2" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice2Id } + } + + It "Does Not Post An Update To Non-Microservices Which Exist In New Relic" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 0 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleWcfApplicationId } + } + } + + Context "When 4 Microservices Are Provided As A Parameter but some don't belong to the environment" { + $microservices = @{ + $SampleMicroserviceName1 = "1.1" + $SampleMicroserviceName2 = "1.1" + $SampleMicroserviceName3 = "1.1" + $SampleMicroserviceName4 = "1.1" + } + + It "Does not throw when proper arguments are provided" { + { + Set-NewRelicDeployment -EnvironmentKey $EnvironmentKey -DeployUser $TestUser -Microservices $microservices + } | Should -Not -Throw + } + + It "Posts An Update To Provided Microservices Which Exist In New Relic - 1" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice1Id } + } + + It "Posts An Update To Provided Microservices Which Exist In New Relic - 2" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SamplePositiveMicroservice2Id } + } + + It "Does Not Post An Update To Provided Microservices Which Do Not Exist In New Relic" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 0 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice1Id } + } + + It "Reports A Warning For Provided Microservices Which Do Not Exist In New Relic - 1" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 0 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice1Id } + } + + It "Reports A Warning For Provided Microservices Which Do Not Exist In New Relic - 2" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context -ParameterFilter { $message.IndexOf("$SampleMicroserviceName3 was specified to be updated in New Relic") -gt -1 } + } + } + + Context "When 4 microservices are provided as a parameter but some don't belong to the (alternate) environment" { + $microservices = @{ + $SampleMicroserviceName1 = "1.1" + $SampleMicroserviceName2 = "1.1" + $SampleMicroserviceName3 = "1.1" + $SampleMicroserviceName4 = "1.1" + } + + It "Does not throw when proper arguments are provided" { + { + Set-NewRelicDeployment -EnvironmentKey $AlternateEnvironmentKey -DeployUser $TestUser -microservices $microservices + } | Should -Not -Throw + } + + It "Only posts to the specified environment - 1" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 1 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice1Id } + } + + It "Only posts to the specified environment - 2" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Submit-DeploymentToNewRelic -Exactly -Times 0 -Scope Context -ParameterFilter { $ApplicationId -eq $SampleNegativeMicroservice2Id } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-RapidFailSettings.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-RapidFailSettings.ps1 new file mode 100644 index 0000000..ae74678 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-RapidFailSettings.ps1 @@ -0,0 +1,47 @@ +function Set-RapidFailSettings { +<# +.SYNOPSIS + Sets the Rapid-Fail Protection settings for all AppPools configured + on the current server for use with AppPool-Shutdown-Alert.exe +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("exePath")] + [string]$autoShutdownExePath = "C:\Tools\AppPool-Shutdown-Alert\AppPool-Shutdown-Alert.exe" + ) + + try{ + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName + $iis = New-Object Microsoft.Web.Administration.ServerManager + + if(!(Test-Path $autoShutdownExePath)) + { + Write-Warning("Path $autoShutdownExePath does not already exist.") + Write-Warning("Verify that AppPool-Shutdown-Alert has been installed with chocolatey or this alert will not work. It will create the path above.") + Write-Warning("Run 'choco install AppPool-Shutdown-Alert -y' on the target server to install it.") + Write-Warning("Application Pool settings will still be changed.`n`n") + } + + foreach ($applicationPool in $iis.ApplicationPools) + { + Write-Host("Current Application Pool: $($applicationPool.Name)") + Write-Host("AutoShutdownExe is set to: $($applicationPool.Failure.AutoShutdownExe)") + Write-Host("Setting AutoShutdownExe to: $autoShutdownExePath") + Write-Host("Setting AutoShutdownParams to: $($applicationPool.Name)`n") + + $applicationPool.Failure.AutoShutdownExe = $autoShutdownExePath + $applicationPool.Failure.AutoShutdownParams = $applicationPool.Name + } + } + catch + { + Write-Error("An error occurred while trying to set Application Pool settings:") + Write-Error $_ + } + + $iis.CommitChanges() + + Write-Host("Application Pool changes committed successfully.`n") +} diff --git a/Modules/Alkami.DevOps.Installation/Public/Set-ServiceAccountValue.ps1 b/Modules/Alkami.DevOps.Installation/Public/Set-ServiceAccountValue.ps1 new file mode 100644 index 0000000..53e9512 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Set-ServiceAccountValue.ps1 @@ -0,0 +1,50 @@ +function Set-ServiceAccountValue { +<# +.SYNOPSIS + Sets a value to the Service Account. +#> + + [CmdletBinding()] + Param( + [Alkami.Ops.SecretServer.Model.User]$serviceAccount + ) + + $logLead = (Get-LogLeadName); + + foreach ($application in $appTierApplications) { + if ($serviceAccount.SecretName -like ("*{0}*" -f $application.Name)) { + if ($serviceAccount.UserName.EndsWith("$")) { + Write-Output ("$logLead : Setting application {0} to install with GMSA user {1}" -f $application.Name, $serviceAccount.UserName) + $application.IsGMSAAccount = $true + $application.Password = "GMSA" + } + else { + Write-Output ("$logLead : Setting application {0} to install with user {1}" -f $application.Name, $serviceAccount.UserName) + $application.IsGMSAAccount = $false + $application.Password = $serviceAccount.Password + } + + $application.User = $serviceAccount.UserName + continue + } + } + + foreach ($appTierService in (Get-AppTierServices)) { + if ($serviceAccount.SecretName -like ("*{0}*" -f $appTierService.FriendlyName)) { + if ($serviceAccount.UserName.EndsWith("$")) { + Write-Output ("$logLead : Setting Windows Service {0} to install with GMSA user {1}" -f $appTierService.FriendlyName, $serviceAccount.UserName) + $appTierService.IsGMSAAccount = $true + $appTierService.Password = "GMSA" + } + else { + Write-Output ("$logLead : Setting application {0} to install with user {1}" -f $appTierService.FriendlyName, $serviceAccount.UserName) + $appTierService.IsGMSAAccount = $false + $appTierService.Password = $serviceAccount.Password + } + + $appTierService.User = $serviceAccount.UserName + continue + } + } +} + diff --git a/Modules/Alkami.DevOps.Installation/Public/Submit-DeploymentToNewRelic.ps1 b/Modules/Alkami.DevOps.Installation/Public/Submit-DeploymentToNewRelic.ps1 new file mode 100644 index 0000000..17fa499 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Submit-DeploymentToNewRelic.ps1 @@ -0,0 +1,65 @@ +function Submit-DeploymentToNewRelic { +<# +.SYNOPSIS + Posts a Deployment to a New Relic application via the API + +.PARAMETER ApiKey + New Relic Api Key + +.PARAMETER ApplicationId + New Relic supplied Application Id + +.PARAMETER AppVersion + Version of the application being updated. + +.PARAMETER DeployUser + Name of person doing the deploy + +.PARAMETER EnvironmentKey + Name of the Environment being deployed to +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string] $ApiKey, + [Parameter(Mandatory = $true)] + [string] $ApplicationId, + [Parameter(Mandatory = $true)] + [string] $AppVersion, + [Parameter(Mandatory = $true)] + [string] $DeployUser, + [Parameter(Mandatory = $true)] + [string] $EnvironmentKey + ) + + $postParams = @{ + "deployment[revision]" = $appVersion; + "deployment[description]" = "Deployment of $($appVersion) to $($environmentKey)"; + "deployment[changelog]" = "Update to $($appVersion)"; + "deployment[user]" = $deployUser + } + + $postUri = ("https://api.newrelic.com/v2/applications/{0}/deployments.xml" -f $applicationId) + Write-Verbose ("$logLead : Posting Deployment Version {0} to URI {1}" -f $appVersion, $postUri) + + try { + $postResponse = Invoke-WebRequest -UseBasicParsing -Uri $postUri -Method Post -Headers @{"X-Api-Key" = $apiKey } -Body $postParams + + if ($postResponse.StatusCode -notmatch "20\d") { + Write-Host "$logLead : Posting to Newrelic returned a status code other than 20x for Uri PostUrl=$postUri StatusCode=$($postResponse.StatusCode) StatusDescription=$($postResponse.StatusDescription)" + return $false + } + } catch [System.Net.WebException] { + $errorMessage = $_.Exception.Message + $contentResponse = (New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() + Write-Warning "$logLead : Could not complete deployment to NewRelic. Error message: [$errorMessage]. Content of response was [$contentResponse]" + return $false + } catch { + $errorMessage = $_.Exception.Message + Write-Warning "$logLead : Could not complete deployment to NewRelic. Error message: [$errorMessage]" + + return $false + } + + return $true +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Uninstall-NewRelicDotNetAgent.ps1 b/Modules/Alkami.DevOps.Installation/Public/Uninstall-NewRelicDotNetAgent.ps1 new file mode 100644 index 0000000..974f2ed --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Uninstall-NewRelicDotNetAgent.ps1 @@ -0,0 +1,19 @@ +function Uninstall-NewRelicDotNetAgent { + <# +.SYNOPSIS + Uninstalls the New Relic .NET Agent +#> + + [CmdletBinding()] + Param() + + $logLead = Get-LogLeadName + $newRelicDotnetAgentPackageName = "newrelic-dotnet" + + # This uninstall command skips the uninstall.ps1 powershell file included in the package + # It instead runs the auto-installer with the added arguements to display no UI, thus requiring no user interaction + $command = "choco uninstall $newRelicDotnetAgentPackageName -yf --skip-scripts --uninstall-arguments=''/qn''" + Write-Host "$loglead : Uninstalling : $newRelicDotnetAgentPackageName" + Write-Host "$loglead : Uninstall Command: $command" + Invoke-Expression -Command $command +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Update-Borg.Tests.ps1 b/Modules/Alkami.DevOps.Installation/Public/Update-Borg.Tests.ps1 new file mode 100644 index 0000000..1cb46e5 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Update-Borg.Tests.ps1 @@ -0,0 +1,60 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe "Update-Borg" { + Mock -CommandName Write-Host -MockWith {} + Mock -CommandName Write-Error -MockWith {} + + $packageData = New-PackageMetadataObject + $packageData.DisableMicroserviceNewRelic = $false + $appPackage1 = New-DummyPackageInstallationData -PackageName "app.package1" -PackageVersion "1.0.0" -IsFullScale + $appPackage2 = New-DummyPackageInstallationData -PackageName "app.package2" -PackageVersion "1.0.0" -IsFullScale + + $packageArray = @($appPackage1, $appPackage2) + + $packageData.AppPackagesToInstall = $packageArray + $packageData.EnvironmentLabel = "UnitTest" + + Mock -CommandName Update-PlatformElementInventory -MockWith { + # No third case necessary; This function doesn't return. Just does its thing silently. + write-host "params: $borgUri, $ApiKey" + + if ($BorgUri -like "*Fake*"){ + throw "Got a fake URI" + } elseif ($ApiKey -like "*Fake*"){ + throw "Got a fake API key" + } + } + + Context "When Given Invalid Borg Connection Info" { + It "Writes an Error on a bad URI" { + { Update-Borg -BorgApiKey "ValidKey" -BorgUri "FakeUri" -PackageMetadata $packageData } | Should -Not -Throw + } + + It "Writes an Error given an empty URI"{ + Update-Borg -BorgApiKey "ValidKey" -BorgUri "" -PackageMetadata $packageData + + Assert-MockCalled -CommandName Write-Error -Scope Context -Times 1 + } + + It "Writes an Error on a bad API Key"{ + { Update-Borg -BorgApiKey "FakeKey" -BorgUri "ValidUri" -PackageMetadata $packageData } | Should -Not -Throw + + } + } + + Context "When Given Valid Info"{ + It "Updates Borg" { + Update-Borg -BorgApiKey "ValidKey" -BorgUri "ValidUri" -PackageMetadata $packageData + + Assert-MockCalled -CommandName Write-Host -Times 1 -ParameterFilter {$object -eq "BoRG successfully updated."} + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Update-Borg.ps1 b/Modules/Alkami.DevOps.Installation/Public/Update-Borg.ps1 new file mode 100644 index 0000000..1e70ab7 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Update-Borg.ps1 @@ -0,0 +1,46 @@ +function Update-Borg{ +<# + .SYNOPSIS + Update Borg with information about packages being installed. Utilized by Classify-Packages. + + .PARAMETER BorgApiKey + API key for borg + .PARAMETER BorgUri + URI of the Borg API + .PARAMETER PackageMetadata + Object containing package information to be sent to Borg. + +#> + [CmdletBinding()] + param( + [string]$BorgApiKey, + [string]$BorgUri, + [PSObject]$PackageMetadata + ) + $borgElementsToAdd = Select-UniqueServerPackages @($PackageMetadata.WebPackagesToInstall, $PackageMetadata.AppPackagesToInstall, $PackageMetadata.MicPackagesToInstall) + $borgElementsToRemove = Select-UniqueServerPackages @($PackageMetadata.WebPackagesToUninstall, $PackageMetadata.AppPackagesToUninstall, $PackageMetadata.MicPackagesToUninstall) + + try { + if (![string]::IsNullOrWhiteSpace($BorgUri)) { + Write-Host "Updating BoRG [$BorgUri] Inventory" + + $splatParams = @{ + ElementsToAdd = $borgElementsToAdd + ElementsToRemove = $borgElementsToRemove + EnvironmentTypeName = $PackageMetadata.EnvironmentLabel + PlatformVersionName = "Unspecified" + ElementTierName = "Unknown" + BorgUri = $BorgUri + ApiKey = $BorgApiKey + } + + Update-PlatformElementInventory @splatParams + + Write-Host "BoRG successfully updated." + } else { + Write-Error "BorgUri is not configured! Check deployment params in TC." + } + } catch { + Write-Error "An exception occurred while updating BoRG. Error: $($_.Exception.Message)" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/Public/Write-PackageList.ps1 b/Modules/Alkami.DevOps.Installation/Public/Write-PackageList.ps1 new file mode 100644 index 0000000..0bbed44 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/Public/Write-PackageList.ps1 @@ -0,0 +1,88 @@ +function Write-PackageList { + <# + .SYNOPSIS + Dump packages to console. Utilized by Classify-Packages. + + .PARAMETER HasBadPackages + Bool detailing whether or not the package list includes packages that are currently installed in the wrong place. + + .PARAMETER PackageMetadata + Packages with data to dump. + + #> + [CmdletBinding()] + param( + $HasBadPackages, + $PackageMetadata + ) + #region Write Packages To Uninstall + # Write out list of packages being removed from the wrong servers. + if ($HasBadPackages) { + Write-Host ("##teamcity[blockOpened name='Packages Being Removed From Incorrect Servers']") + foreach ($package in $PackageMetadata.BadWebPackagesToUninstall) { + Write-Host "Removing $($package.Name) from Web servers." + } + foreach ($package in $PackageMetadata.BadAppPackagesToUninstall) { + Write-Host "Removing $($package.Name) from App servers." + } + foreach ($package in $PackageMetadata.BadMicPackagesToUninstall) { + Write-Host "Removing $($package.Name) from Mic/Fab servers." + } + Write-Host ("##teamcity[blockClosed name='Packages Being Removed From Incorrect Servers']") + } + #endregion Write Packages To Uninstall + + #region Write Packages To Install + if ($PackageMetadata.HasWebInstalls -or $PackageMetadata.HasWebUninstalls -or + $PackageMetadata.HasAppInstalls -or $PackageMetadata.HasAppUninstalls -or + $PackageMetadata.HasMicInstalls -or $PackageMetadata.HasMicUninstalls) { + Write-Host ("##teamcity[blockOpened name='Packages to Install']") + + # Write out web packages. + if ($PackageMetadata.HasWebInstalls) { + Write-Host ("##teamcity[blockOpened name='Web Packages to Install']") + Write-InstallPackageMetadataToConsole -Packages $PackageMetadata.WebPackagesToInstall -isWeb + Write-Host ("##teamcity[blockClosed name='Web Packages to Install']") + } + if ($PackageMetadata.HasWebUninstalls) { + Write-Host ("##teamcity[blockOpened name='Web Packages to Uninstall']") + foreach ($package in $PackageMetadata.WebPackagesToUninstall) { + Write-Host $package.Name + } + Write-Host ("##teamcity[blockClosed name='Web Packages to Uninstall']") + } + + # Write out app packages. + if ($PackageMetadata.HasAppInstalls) { + Write-Host ("##teamcity[blockOpened name='App Packages to Install']") + Write-InstallPackageMetadataToConsole -Packages $PackageMetadata.AppPackagesToInstall + Write-Host ("##teamcity[blockClosed name='App Packages to Install']") + } + if ($PackageMetadata.HasAppUninstalls) { + Write-Host ("##teamcity[blockOpened name='App Packages to Uninstall']") + foreach ($package in $PackageMetadata.AppPackagesToUninstall) { + Write-Host $package.Name + } + Write-Host ("##teamcity[blockClosed name='App Packages to Uninstall']") + } + + # Write out mic packages. + if ($PackageMetadata.HasMicInstalls) { + Write-Host ("##teamcity[blockOpened name='Mic Packages to Install']") + Write-InstallPackageMetadataToConsole -Packages $PackageMetadata.MicPackagesToInstall + Write-Host ("##teamcity[blockClosed name='Mic Packages to Install']") + } + if ($PackageMetadata.HasMicUninstalls) { + Write-Host ("##teamcity[blockOpened name='Mic Packages to Uninstall']") + foreach ($package in $PackageMetadata.MicPackagesToUninstall) { + Write-Host $package.Name + } + Write-Host ("##teamcity[blockClosed name='Mic Packages to Uninstall']") + } + + Write-Host ("##teamcity[blockClosed name='Packages to Install']") + } else { + Write-Host "No packages to install or uninstall." + } + #endregion Write Packages To Install +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Installation/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Installation/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Installation/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.Installation/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.nuspec b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.nuspec new file mode 100644 index 0000000..2dbcab2 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.nuspec @@ -0,0 +1,34 @@ + + + + Alkami.DevOps.Inventory + $version$ + Alkami Platform Modules - DevOps - Inventory + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Inventory + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps Inventory module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.psd1 b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.psd1 new file mode 100644 index 0000000..46b04b5 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.psd1 @@ -0,0 +1,19 @@ +@{ + RootModule = 'Alkami.DevOps.Inventory.psm1' + ModuleVersion = '1.7.1' + GUID = '03fd4b8d-43a9-42a3-bf94-783fb89fae76' + Author = 'SRE,dsage' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A set of cmdlets used to pull machine details' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.IIS','Alkami.PowerShell.Configuration','Alkami.DevOps.Common','Alkami.PowerShell.AD','Alkami.PowerShell.Services','Alkami.PowerShell.Choco' + FunctionsToExport = 'Get-AlkamiInstallationDriveInventory','Get-ApplicationInventory','Get-AppSettingsInventory','Get-AppSettingsInventoryConfigPaths','Get-CertificateInventory','Get-ChocolateyInventory','Get-ComputerUptime','Get-ConnectionStringInventory','Get-DotNetTempFilesCreationTime','Get-EnvironmentalVariables','Get-FileSystemInventory','Get-IISInventory','Get-IISResetHistory','Get-LocalSecurityPolicyInventory','Get-MachineInventory','Get-MemoryInventory','Get-ModuleInventory','Get-OrbInventory','Get-PlatformElementInventory','Get-PlatformInventory','Get-ProcessorInventory','Get-RestartHistory','Get-ServiceFabricInventory','Get-SystemWebSettingsInventory','Get-TimeConfiguration','Get-WindowsFeatureInventory','Get-WindowsServiceInventory','New-PlatformElementDetailPSObject','New-PlatformElementDetails','Remove-PlatformElementDetailById','Remove-PlatformElementDetails','Save-MachineManifest','Send-DeploymentManifestToS3','Sync-PlatformElementInventory','Update-PlatformElementInventory' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'inventory', 'sre') + ProjectUri = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Inventory' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Inventory' +} diff --git a/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.pssproj b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.pssproj new file mode 100644 index 0000000..ffdfea3 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Alkami.DevOps.Inventory.pssproj @@ -0,0 +1,82 @@ + + + Debug + 2.0 + {54d03dc1-d279-4278-842e-db0500e33196} + Exe + MyApplication + MyApplication + Alkami.DevOps.Inventory + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/AlkamiManifest.xml b/Modules/Alkami.DevOps.Inventory/AlkamiManifest.xml new file mode 100644 index 0000000..af4f5f6 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Inventory + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Inventory/Private/Add-CertificatesToInventoryDictionary.ps1 b/Modules/Alkami.DevOps.Inventory/Private/Add-CertificatesToInventoryDictionary.ps1 new file mode 100644 index 0000000..0640a7f --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Private/Add-CertificatesToInventoryDictionary.ps1 @@ -0,0 +1,32 @@ +function Add-CertificatesToInventoryDictionary { +<# +.SYNOPSIS + Adds Certificates to the Inventory Dictionary. +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Object[]]$certificates + ) + + $logLead = (Get-LogLeadName); + $dictionary = New-Object System.Collections.Specialized.OrderedDictionary + + foreach ($cert in $certificates | Group-Object {$_.Thumbprint}) { + + $uniqueCert = $cert.Group | Select-Object -First 1 + $certPropertyHash = Get-CertificatePropertyHash $uniqueCert + + if ($cert.Count -gt 1) { + + $thumbprint = $uniqueCert.Thumbprint + Write-Warning "$logLead : Certificate With Thumbprint $thumbprint Has a Duplicate Record" + $certPropertyHash.Add("HasDuplicate", $true) + } + + $dictionary.Add($uniqueCert.Thumbprint.ToString(), $certPropertyHash) + } + + return $dictionary +} diff --git a/Modules/Alkami.DevOps.Inventory/Private/Get-CertificatePropertyHash.ps1 b/Modules/Alkami.DevOps.Inventory/Private/Get-CertificatePropertyHash.ps1 new file mode 100644 index 0000000..987d1db --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Private/Get-CertificatePropertyHash.ps1 @@ -0,0 +1,49 @@ +function Get-CertificatePropertyHash { +<# +.SYNOPSIS + Fetches a Certificate's Property Hash. +#> + + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate + ) + + $logLead = (Get-LogLeadName); + $currentDate = (Get-Date); + $certIsExpired = ($certificate.NotAfter -gt $currentDate) + + $usersWithPermsArray = $null + if ($certificate.HasPrivateKey) { + + Write-Verbose "$logLead : Searching for Private Key Details for Certificate $($certificate.Thumbprint)" + $ACLs = (Get-PrivateKeyPermissions $certificate) + $usersWithPermsArray = ($ACLs | Select-Object -ExpandProperty IdentityReference) + + $usersWithPerms = New-Object System.Collections.Specialized.OrderedDictionary + + for ($i = 0; $i -lt $usersWithPermsArray.Count; $i++) { + + $usersWithPerms.Add($i.ToString(), $usersWithPermsArray[$i]) + } + } + + return @{ + + SubjectName = $certificate.SubjectName.Name; + DnsNameList = $certificate.DnsNameList.Unicode; + Subject = $certificate.Subject; + Issuer = $certificate.Issuer; + FriendlyName = $certificate.FriendlyName; + HasPrivateKey = $certificate.HasPrivateKey; + NotBefore = $certificate.NotBefore; + NotAfter = $certificate.NotAfter; + SerialNumber = $certificate.SerialNumber; + Thumbprint = $certificate.Thumbprint; + IsExpired = $certIsExpired; + IsCurrent = ($certificate.NotBefore -lt $currentDate -and !$certIsExpired ); + UsersWithPrivateKeyRights = $usersWithPerms; + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.Inventory/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..a671e92 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Private/VariableDeclarations.ps1 @@ -0,0 +1,41 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +try { + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName +} +catch { + try { + # Just in case we have IIS Express and IIS loaded (dev machines) + [System.Reflection.Assembly]::LoadFile("C:\Windows\system32\inetsrv\Microsoft.Web.Administration.dll") + } catch { + # Do nothing in case this is a brand new server + Write-Warning "[Alkami.DevOps.Inventory] : Unable to Load Assembly Microsoft.Web.Administration. Some functions may not work as expected." + } +} + +$inventoryFilterSetOptions = @( + @{ FilterName = "Uptime"; FunctionName = "Get-ComputerUptime"; SectionVariable = "SystemData"; } + @{ FilterName = "EnvironmentVariables"; FunctionName = "Get-EnvironmentalVariables"; SectionVariable = "SystemData"; } + @{ FilterName = "Memory"; FunctionName = "Get-MemoryInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "Modules"; FunctionName = "Get-ModuleInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "Processors"; FunctionName = "Get-ProcessorInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "RestartHistory"; FunctionName = "Get-RestartHistory"; SectionVariable = "SystemData"; } + @{ FilterName = "IISResetHistory"; FunctionName = "Get-IISResetHistory"; SectionVariable = "SystemData"; } + @{ FilterName = "DotNetTempFiles"; FunctionName = "Get-DotNetTempFilesCreationTime"; SectionVariable = "SystemData"; } + @{ FilterName = "Time"; FunctionName = "Get-TimeConfiguration"; SectionVariable = "SystemData"; } + @{ FilterName = "Services"; FunctionName = "Get-WindowsServiceInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "Features"; FunctionName = "Get-WindowsFeatureInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "FileSystem"; FunctionName = "Get-FileSystemInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "Applications"; FunctionName = "Get-ApplicationInventory"; SectionVariable = "SystemData"; } + @{ FilterName = "OrbVersion"; FunctionName = "Get-OrbInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "AppSettings"; FunctionName = "Get-AppSettingsInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "ConnectionStrings"; FunctionName = "Get-ConnectionStringInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "IIS"; FunctionName = "Get-IISInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "Chocolatey"; FunctionName = "Get-ChocolateyInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "ServiceFabric"; FunctionName = "Get-ServiceFabricInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "Certificates"; FunctionName = "Get-CertificateInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "SecurityPolicy"; FunctionName = "Get-LocalSecurityPolicyInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "SystemWebSettings"; FunctionName = "Get-SystemWebSettingsInventory"; SectionVariable = "ConfigData"; } + @{ FilterName = "AlkamiInstallationDrive"; FunctionName = "Get-AlkamiInstallationDriveInventory"; SectionVariable = "ConfigData"; } +) \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-AlkamiInstallationDriveInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-AlkamiInstallationDriveInventory.ps1 new file mode 100644 index 0000000..3ef5b00 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-AlkamiInstallationDriveInventory.ps1 @@ -0,0 +1,19 @@ +function Get-AlkamiInstallationDriveInventory { +<# +.SYNOPSIS + Get the drive letter (and colon) that describes where Alkami Platform software is installed and return the value in a dictionary format. + +.DESCRIPTION + Using Environment Variables, determine where the Alkami Platform software is installed. + If present and in the correct format, ENV:ALKAMI_INSTALLATION_DRIVE is used. Otherwise, + ENV:SystemDrive is used. + +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param () + + $driveLetter = Get-AlkamiInstallationDrive + $returnValue = @{AlkamiInstallationDrive = $driveLetter} + return $returnValue +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.ps1 new file mode 100644 index 0000000..bca396c --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.ps1 @@ -0,0 +1,101 @@ +function Get-AppSettingsInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the App Settings Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $appSettingsDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $appSettingsDetails = New-Object System.Collections.Specialized.OrderedDictionary + + [array]$configs = (Get-AppSettingsInventoryConfigPaths) + + Write-Verbose "$logLead : Found these config paths for processing [$($configs -join ',')]" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Processing App Configs" + + try { + foreach ($config in $configs) { + Write-Verbose "$logLead : Collecting inventory from $($config)" + + # Filter out Choco configs which are not for the parent package + if ($config.DirectoryName -match "chocolatey") { + + $packageName = ($config.Directory.FullName).Split("\") | Where-Object {$_.ToString().StartsWith("Alkami")} | Select-Object -First 1 + if ($null -ne $packageName -and $config.FullName -notmatch $packageName -and $config.Name -notmatch "machine.config") { + + Write-Verbose ("$logLead : [$($providerStopWatch.Elapsed)] : {0} does not match package name {1} and will be skipped" -f $config.Name, $packageName) + continue; + } + } + + if ($config.DirectoryName -match "ORB") { + + # Handle Legacy App Configs Which May Conflict on Name + $parentKey = ($config.DirectoryName.Split("\") | Select-Object -Last 1) + } + else { + + # The machine.config and choco configs should be unique by name + $parentKey = $config.Name.Replace(".exe.config", "") + } + + Write-Verbose "$logLead : found `$parentKey to be [$parentKey]" + + $appSettingsDetails[$parentKey] = New-Object System.Collections.Specialized.OrderedDictionary + $appSettingsDetails[$parentKey]["FilePath"] = $config.FullName + + [xml]$configXml = Read-XmlFile $config.FullName + if ($null -eq $configXml) { + Write-Verbose "$logLead : The Content of the Specified File [$($config.FullName)] is Null, or Could Not be Cast to XML, or could not be found" + $appSettingsDetails[$parentKey]["ERROR"] = "The Content of the Specified File is Null, or Could Not be Cast to XML, or could not be found" + } else { + $appSettings = $configXml.SelectSingleNode("//appSettings") + + if ($null -eq $appSettings -or !$appSettings.HasChildNodes) { + Write-Verbose ("$logLead : [$($providerStopWatch.Elapsed)] : No AppSettings Found in {0}" -f $config.Name) + continue; + } + + + foreach ($appSetting in ($appSettings.ChildNodes | Where-Object {$_.NodeType -ne "Comment"})) { + try { + if ($appSetting.key -match "password" -or $appSetting.value -match "password") { + if ([string]::IsNullOrEmpty($appSetting.value)) { + $appSetting.Value = "EMPTY" + } + elseif ($appSetting.value -match "REPLACEME") { + $appSetting.Value = "UNSET" + } + else { + $appSetting.Value = "HIDDEN" + } + } + + $appSettingsDetails[$parentKey][$appSetting.key] = $appSetting.value + } catch { + Write-Verbose "could not capture value from [$($appSetting.OuterXml)]" + } + } + } + } + } + catch { + Write-Verbose $_.Exception.ToString() + $appSettingsDetails["Error"] = $_.Exception.ToString() + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Processing App Configs" + + $appSettingsDictionary.Add("AppSettings", $appSettingsDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $appSettingsDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.tests.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.tests.ps1 new file mode 100644 index 0000000..6f62ba3 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventory.tests.ps1 @@ -0,0 +1,118 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AppSettingsInventory" { + + Context "When the app setting values are good" { + + ## Good file values + $GoodValues = @' + + + + + + + +'@ + + $global:TempFile = New-TemporaryFile + + Set-Content -Path $global:TempFile.FullName -Value $GoodValues + + Mock -ModuleName $moduleForMock -CommandName Get-AppSettingsInventoryConfigPaths { + return @(Get-Item $global:TempFile.FullName) + } + + + It "Works as expected" { + $a = (Get-AppSettingsInventory) + $a.Values.Values[1] | Should Be "value1" + $a.Values.Values[2] | Should Be "value2" + ## Keys is an ICollection, and it doesn't know how to index otherwise. + ## By forcing it to an array before I index it, I can read the values I expect by ID + ([string[]]$a.Values.Values.Keys)[1] | Should Be "key1" + ([string[]]$a.Values.Values.Keys)[2] | Should Be "key2" + + ## There's a filename key and the two of our object + ([string[]]$a.Values.Values.Keys).Length | Should Be 3 + } + + Remove-Item $global:TempFile.FullName -Force + } + + Context "When the app setting values are empty" { + + ## Good file values + $GoodValues = @' + + + + + +'@ + + $global:TempFile = New-TemporaryFile + + Set-Content -Path $global:TempFile.FullName -Value $GoodValues + + Mock -ModuleName $moduleForMock -CommandName Get-AppSettingsInventoryConfigPaths { + return @(Get-Item $global:TempFile.FullName) + } + + + It "Works as expected" { + $a = (Get-AppSettingsInventory) + ## There's a filename key even when there are no KVP + ([string[]]$a.Values.Values.Keys).Length | Should Be 1 + } + + Remove-Item $global:TempFile.FullName -Force + } + + Context "When the app setting values are good but have a clear entry" { + + ## Good file values + $GoodValues = @' + + + + + + + + +'@ + + $global:TempFile = New-TemporaryFile + + Set-Content -Path $global:TempFile.FullName -Value $GoodValues + + Mock -ModuleName $moduleForMock -CommandName Get-AppSettingsInventoryConfigPaths { + return @(Get-Item $global:TempFile.FullName) + } + + + It "Works as expected" { + $a = (Get-AppSettingsInventory) + $a.Values.Values[1] | Should Be "value1" + $a.Values.Values[2] | Should Be "value2" + ## Keys is an ICollection, and it doesn't know how to index otherwise. + ## By forcing it to an array before I index it, I can read the values I expect by ID + ([string[]]$a.Values.Values.Keys)[1] | Should Be "key1" + ([string[]]$a.Values.Values.Keys)[2] | Should Be "key2" + + ## There's a filename key and the two of our object + ([string[]]$a.Values.Values.Keys).Length | Should Be 3 + } + + Remove-Item $global:TempFile.FullName -Force + } + +} + diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventoryConfigPaths.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventoryConfigPaths.ps1 new file mode 100644 index 0000000..801dcb0 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-AppSettingsInventoryConfigPaths.ps1 @@ -0,0 +1,37 @@ +function Get-AppSettingsInventoryConfigPaths { +<# +.SYNOPSIS + Collects the anticipated config paths for AppSettings Inventorying + +.DESCRIPTION + Collects the existing config files from + * C:\ProgramData\Chocolatey\lib + * Get-ChildItem (Get-OrbPath) + * Get-ChildItem (Get-DotNetConfigPath -use64Bit:$true) +#> + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + [array]$configs = @() + $chocoInstallPath = Get-ChocolateyInstallPath + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Choco App Configs" + $configs += Get-ChildItem "$chocoInstallPath\lib" -File -Recurse -Filter Alkami*.exe.config + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Choco App Configs" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading ORB App|Web Configs" + $configs += Get-ChildItem (Get-OrbPath) -File -Recurse -Filter *.config -Depth 1 ` + | Where-Object {$_.Directory.Attributes.ToString() -notmatch "ReparsePoint" -and $_.Name -notmatch "log4net"} + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading ORB App|Web Configs" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Machine Config" + $configs += Get-ChildItem (Get-DotNetConfigPath) -File + $providerStopWatch.Stop() + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Machine Config" + + return $configs +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ApplicationInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ApplicationInventory.ps1 new file mode 100644 index 0000000..92a2f04 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ApplicationInventory.ps1 @@ -0,0 +1,44 @@ +function Get-ApplicationInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Application Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $applicationDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $applicationDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading App Uninstall Details" + $uninstallDetails = Get-ItemProperty -Path HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : App Uninstall Details Retrieved" + + foreach ($app in $uninstallDetails) { + + $appName = $app | Select-Object -ExpandProperty PSChildName + $applicationDetails[$appName] = New-Object System.Collections.Specialized.OrderedDictionary + $applicationDetails[$appName]["Name"] = $app.DisplayName + $applicationDetails[$appName]["Version"] = $app.DisplayVersion + $applicationDetails[$appName]["Publisher"] = $app.Publisher + $applicationDetails[$appName]["InstallDate"] = $app.InstallDate + $applicationDetails[$appName]["UninstallString"] = $app.UninstallString + } + } + catch { + + $applicationDetails["Error"] = $_.Exception.ToString() + } + + $applicationDictionary.Add("Software", $applicationDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $applicationDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-CertificateInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-CertificateInventory.ps1 new file mode 100644 index 0000000..72d0340 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-CertificateInventory.ps1 @@ -0,0 +1,53 @@ +function Get-CertificateInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Certificate Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $certDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $storeDictionary = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Machine Certificates" + $personalCerts = Get-ChildItem -Path Cert:\LocalMachine\My + $iaCerts = Get-ChildItem -Path Cert:\LocalMachine\CA + $rootCerts = Get-ChildItem -Path Cert:\LocalMachine\Root + $trustedPeopleCerts = Get-ChildItem -Path Cert:\LocalMachine\TrustedPeople + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Machine Certificates Received" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Working on Personal Certs" + $personalCertDictionary = Add-CertificatesToInventoryDictionary $personalCerts + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Working on IA Certs" + $iaCertDictionary = Add-CertificatesToInventoryDictionary $iaCerts + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Working on Root Certs" + $rootCertDictionary = Add-CertificatesToInventoryDictionary $rootCerts + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Working on Trusted People Certs" + $trustedPeopleCertDictionary = Add-CertificatesToInventoryDictionary $trustedPeopleCerts + + $storeDictionary.Add("Personal", $personalCertDictionary) + $storeDictionary.Add("IA", $iaCertDictionary) + $storeDictionary.Add("Root", $rootCertDictionary) + $storeDictionary.Add("TrustedPeople", $trustedPeopleCertDictionary) + } + catch { + + $storeDictionary["Error"] = $_.Exception.ToString() + } + + $certDictionary.Add("Certificates", $storeDictionary) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $certDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ChocolateyInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ChocolateyInventory.ps1 new file mode 100644 index 0000000..5426980 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ChocolateyInventory.ps1 @@ -0,0 +1,102 @@ +function Get-ChocolateyInventory { + <# + .SYNOPSIS + Get a PSObject containing categorized packages which represents the assumed list of what is installed on a machine, per BoRG + + .EXAMPLE + Get-ChocolateyInventory + + .PARAMETER WithTags + Switch indicating whether or not to include package tags + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [switch]$WithTags + ) + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $chocoDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $chocoDetails = New-Object System.Collections.Specialized.OrderedDictionary + $chocoPackages = New-Object System.Collections.Specialized.OrderedDictionary + $chocoSettings = New-Object System.Collections.Specialized.OrderedDictionary + $chocoSources = New-Object System.Collections.Specialized.OrderedDictionary + $requiredChocoPackages = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Choco Sources" + $sources = Get-ChocolateySources + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Choco Sources" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Choco Packages" + $localChocos = Get-ChocoState -l -getServiceInfo + Set-ChocoPackageSourceFeeds $localChocos + if ($WithTags.IsPresent) { + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Retrieving tags" + Set-ChocoPackageTags $localChocos + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done retrieving tags" + } + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Choco Packages" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Miscellaneous Choco Info" + $chocoVersion = choco --version -r + $chocoFeatures = choco features -r + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Miscellaneous Choco Info" + + # Handle Version + $chocoDictionary["ChocolateyVersion"] = $chocoVersion + + # Handle Features + foreach ($feature in $chocoFeatures) { + + $featureSplit = $feature.Split("|") + $chocoSettings.Add($featureSplit[0], $featureSplit[1]) + } + + # Handle Sources + foreach ($source in $sources) { + + $chocoSources.Add($source.Name, ($source | Select-Object Source, IsDefault, Priority, Disabled, IsSdk)) + } + + $chocoInstallPath = Get-ChocolateyInstallPath + $chocoFolder = Join-Path $chocoInstallPath "lib" + # Handle Packages + foreach ($package in $localChocos) { + # I dislike putting this here, but I don't think it's worth extracting. Maybe. + $packageFolder = Join-Path $chocoFolder "$($package.Name)" + $nuspecPath = Join-Path $packageFolder "$($package.Name).nuspec" + [xml]$nuspecContent = Read-XMLFile -xmlPath $nuspecPath + $nuspecVersion = $nuspecContent.Package.Metadata.Version + + $packageResult = @{ + Name = $package.Name + IsSDK = $package.Feed.IsSDK; + IsService = $package.IsService; + SourceName = $package.Feed.Name; + SourceUrl = $package.Feed.Source; + StartMode = $package.StartMode; + Tags = $package.Tags; + Version = $package.Version; + NuSpecVersion = $nuspecVersion; + } + + $chocoPackages.Add($package.Name, (New-Object PSObject -Property $packageResult)) + } + + # This property is legacy from SRE-13518 and can probably go if you're doing future work + # We left it in case someone does $x.requiredChocoPackages.Name or somesuch, so it's not _$null_ + $chocoDetails.Add("requiredChocoPackages", $requiredChocoPackages) + $chocoDetails.Add("Packages", $chocoPackages) + $chocoDetails.Add("Settings", $chocoSettings) + $chocoDetails.Add("Sources", $chocoSources) + + $chocoDictionary.Add("Chocolatey", $chocoDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $chocoDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ComputerUptime.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ComputerUptime.ps1 new file mode 100644 index 0000000..7d7d753 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ComputerUptime.ps1 @@ -0,0 +1,42 @@ +function Get-ComputerUptime { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Computer Up Time. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $uptimeDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $uptimeDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Computer Uptime" + $uptimeSeconds = Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_PerfFormattedData_PerfOS_System -Property SystemUpTime | Select-Object -ExpandProperty SystemUpTime + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Computer Uptime Retrieved" + + $uptimeTimeSpan = New-TimeSpan -Seconds $uptimeSeconds + $bootTime = (Get-Date).AddSeconds(-1 * $uptimeSeconds) + + $uptimeDetails["UptimeString"] = ("{0:00}d {1:00}h {2:00}m {3:00}s" -f $uptimeTimeSpan.Days, + $uptimeTimeSpan.Hours, $uptimeTimeSpan.Minutes, $uptimeTimeSpan.Seconds); + $uptimeDetails["UptimeSeconds"] = $uptimeSeconds; + $uptimeDetails["LastBootTime"] = ("{0} {1}" -f $bootTime.ToShortDateString(), $bootTime.ToLongTimeString()); + $uptimeDetails["LastBootTimeTicks"] = $bootTime.Ticks + } + catch { + + $uptimeDetails["Error"] = $_.Exception.ToString() + } + + $uptimeDictionary.Add("Uptime", $uptimeDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $uptimeDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ConnectionStringInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ConnectionStringInventory.ps1 new file mode 100644 index 0000000..2b93f09 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ConnectionStringInventory.ps1 @@ -0,0 +1,61 @@ +function Get-ConnectionStringInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Connection String Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $connectionStringDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $connectionStringDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Machine Config Data" + + [xml]$machineConfigRaw = Read-MachineConfig + $connectionStrings = $machineConfigRaw.SelectSingleNode("//connectionStrings") + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Machine Config Data Retrieved" + + try { + + foreach ($connectionString in $connectionStrings.ChildNodes) { + + try { + + $conn = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($connectionString.connectionString) -ErrorAction SilentlyContinue + } + catch { + + # Non-SQL Connection Strings, like Redis, will error here. And that's OK + Write-Host "$logLead : Non-SQL Connection Strings, like Redis, will error here. And that's OK." + } + + if ($null -ne $conn -and !([String]::IsNullOrEmpty($conn.Password))) { + + $cleansedConnectionString = $conn.ToString().Replace($conn.Password, "HIDDEN") + } + else { + + $cleansedConnectionString = ($connectionString.connectionString -replace "password\=[^;]+", "password=HIDDEN") + } + + $connectionStringDetails[$connectionString.name] = New-Object System.Collections.Specialized.OrderedDictionary + $connectionStringDetails[$connectionString.name]["ConnectionString"] = $cleansedConnectionString + } + } + catch { + + $connectionStringDetails["Error"] = $_.Exception.ToString() + } + + $connectionStringDictionary.Add("ConnectionStrings", $connectionStringDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $connectionStringDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-DotNetTempFilesCreationTime.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-DotNetTempFilesCreationTime.ps1 new file mode 100644 index 0000000..fd9d85c --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-DotNetTempFilesCreationTime.ps1 @@ -0,0 +1,44 @@ +function Get-DotNetTempFilesCreationTime { +<# +.SYNOPSIS + Gets the first time a .NET Temporary File was created +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("AppPoolName")] + [string]$Name = "" + ) + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + + $DotNetTempFilesCreationTimeDictonary = New-Object System.Collections.Specialized.OrderedDictionary + $DotNetTempFilesCreationTimeDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting ASP.NET Temporary files" + $targetItems = Get-ChildItem -recurse "C:\Windows\Microsoft.NET\Framework*\v*\Temporary ASP.NET Files\$Name*"; + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Retrived ASP.NET Temporary files" + + $LastWriteTimes = @() + foreach ($targetItem in $targetItems) { + Write-Verbose ("$logLead : Extracting Create time {0} for file {1}" -f $targetItem.creationTime, $targetItem.Name); + $LastWriteTimes += $targetItem.creationTime + } + + $DotNetTempFilesCreationTimeDetails["Date"] = ($LastWriteTimes | sort-object)[0] + + } catch { + + $DotNetTempFilesCreationTimeDetails["Error"] = $_.Exception.ToString() + } + + $DotNetTempFilesCreationTimeDictonary.Add("DotNetTemps", $DotNetTempFilesCreationTimeDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $DotNetTempFilesCreationTimeDictonary +} diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-EnvironmentalVariables.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-EnvironmentalVariables.ps1 new file mode 100644 index 0000000..5ef06e7 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-EnvironmentalVariables.ps1 @@ -0,0 +1,36 @@ +function Get-EnvironmentalVariables { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Environmental Variables. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $environmentVariablesDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $environmentVariableDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Environmental Variables" + + try { + + Get-ChildItem ENV: | ForEach-Object { + + $environmentVariableDetails.Add($_.Name, $_.Value) + } + } + catch { + + $environmentVariableDetails["Error"] = $_.Exception.Message + } + + $environmentVariablesDictionary.Add("EnvironmentalVariables", $environmentVariableDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $environmentVariablesDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-FileSystemInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-FileSystemInventory.ps1 new file mode 100644 index 0000000..c1d5e8b --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-FileSystemInventory.ps1 @@ -0,0 +1,51 @@ +function Get-FileSystemInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the FileSystem Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $fileSystemDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $fileSystemDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Set-Variable megaByteDivisor -Option Constant -Value 1048576 + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting File System Details" + $diskInfo = Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_LogicalDisk -Property DeviceID, Name, FileSystem, VolumeName, Size, FreeSpace + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : File System Details Retrieved" + + foreach ($disk in $diskInfo) { + $deviceId = $disk.DeviceID + $fileSystemDetails[$deviceId] = New-Object System.Collections.Specialized.OrderedDictionary + $fileSystemDetails[$deviceId]['Mount'] = $disk.CimInstanceProperties['Name'].Value + $fileSystemDetails[$deviceId]['Type'] = ($disk.CimInstanceProperties['FileSystem'].Value) + $fileSystemDetails[$deviceId]['Name'] = $disk.CimInstanceProperties['VolumeName'].Value + $fileSystemDetails[$deviceId]['SizeMB'] = $disk.CimInstanceProperties['Size'].Value / $megaByteDivisor + $fileSystemDetails[$deviceId]['AvailableMB'] = $disk.CimInstanceProperties['FreeSpace'].Value / $megaByteDivisor + $fileSystemDetails[$deviceId]['UsedMB'] = $fileSystemDetails[$deviceId]['SizeMB'] - $fileSystemDetails[$deviceId]['AvailableMB'] + $fileSystemDetails[$deviceId]['UsedPercent'] = ` + if ($fileSystemDetails[$deviceId]['SizeMB'] -ne 0) { + [System.Math]::Round($fileSystemDetails[$deviceId]['UsedMB'] * 100 / $fileSystemDetails[$deviceId]['SizeMB'], 2) + } else { + 0 + } + } + } catch { + + $fileSystemDetails["Error"] = $_.Exception.ToString() + } + + $fileSystemDictionary.Add('FileSystem', $fileSystemDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $fileSystemDictionary +} diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-IISInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-IISInventory.ps1 new file mode 100644 index 0000000..6d9452c --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-IISInventory.ps1 @@ -0,0 +1,176 @@ +function Get-IISInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the IIS Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $mgr = New-Object Microsoft.Web.Administration.ServerManager + $iisDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $iisDetails = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Parsing IIS Details" + + try { + + foreach ($appPool in $mgr.ApplicationPools) { + + $appPoolName = $appPool.Name + + $appPoolDetails[$appPoolName] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails[$appPoolName]["CLRVersion"] = $appPool.ManagedRuntimeVersion + $appPoolDetails[$appPoolName]["Enable32BitAppOnWin64"] = $appPool.Enable32BitAppOnWin64 + $appPoolDetails[$appPoolName]["ManagedPipelineMode"] = $appPool.ManagedPipelineMode + $appPoolDetails[$appPoolName]["QueueLength"] = $appPool.QueueLength + $appPoolDetails[$appPoolName]["StartMode"] = $appPool.StartMode + $appPoolDetails[$appPoolName]["State"] = $appPool.State + $appPoolDetails[$appPoolName]["AutoStart"] = $appPool.AutoStart + + $appPoolDetails.$appPoolName["CPU"] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails.$appPoolName["CPU"]["Limit"] = $appPool.Cpu.Limit + $appPoolDetails.$appPoolName["CPU"]["LimitAction"] = $appPool.Cpu.Action + $appPoolDetails.$appPoolName["CPU"]["LimitInterval"] = ($appPool.Cpu.ResetInterval).ToString() + $appPoolDetails.$appPoolName["CPU"]["AffinityEnabled"] = $appPool.Cpu.SmpAffinitized + $appPoolDetails.$appPoolName["CPU"]["AffinityMask"] = $appPool.Cpu.SmpProcessorAffinityMask + $appPoolDetails.$appPoolName["CPU"]["AffinityMask64"] = $appPool.Cpu.SmpProcessorAffinityMask2 + + $appPoolDetails.$appPoolName["ProcessModel"] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails.$appPoolName["ProcessModel"]["EventLogEntry"] = $appPool.ProcessModel.LogEventOnProcessModel + $appPoolDetails.$appPoolName["ProcessModel"]["IdentityType"] = $appPool.ProcessModel.IdentityType + $appPoolDetails.$appPoolName["ProcessModel"]["UserName"] = $appPool.ProcessModel.UserName + $appPoolDetails.$appPoolName["ProcessModel"]["IdleTimeout"] = ($appPool.ProcessModel.IdleTimeout).ToString() + $appPoolDetails.$appPoolName["ProcessModel"]["IdleTimeoutAction"] = $appPool.ProcessModel.IdleTimeoutAction + $appPoolDetails.$appPoolName["ProcessModel"]["LoadUserProfile"] = $appPool.ProcessModel.LoadUserProfile + $appPoolDetails.$appPoolName["ProcessModel"]["MaximumWorkerProcesses"] = $appPool.ProcessModel.MaxProcesses + $appPoolDetails.$appPoolName["ProcessModel"]["PingEnabled"] = $appPool.ProcessModel.PingingEnabled + $appPoolDetails.$appPoolName["ProcessModel"]["PingMaxResponseTime"] = ($appPool.ProcessModel.PingInterval).ToString() + $appPoolDetails.$appPoolName["ProcessModel"]["PingPeriod"] = ($appPool.ProcessModel.PingResponseTime).ToString() + $appPoolDetails.$appPoolName["ProcessModel"]["ShutdownTimeLimit"] = ($appPool.ProcessModel.ShutdownTimeLimit).ToString() + $appPoolDetails.$appPoolName["ProcessModel"]["StartupTimeLimit"] = ($appPool.ProcessModel.StartupTimeLimit).ToString() + + $appPoolDetails.$appPoolName["ProcessOrphaning"] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails.$appPoolName["ProcessOrphaning"]["OrphanWorkerProcess"] = $appPool.Failure.OrphanWorkerProcess + $appPoolDetails.$appPoolName["ProcessOrphaning"]["Executable"] = $appPool.Failure.OrphanActionExe + $appPoolDetails.$appPoolName["ProcessOrphaning"]["ExecutableParams"] = $appPool.Failure.OrphanActionParams + + $appPoolDetails.$appPoolName["RapidFailProtection"] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails.$appPoolName["RapidFailProtection"]["ReponseType"] = $appPool.Failure.LoadBalancerCapabilities + $appPoolDetails.$appPoolName["RapidFailProtection"]["Enabled"] = $appPool.Failure.RapidFailProtection + $appPoolDetails.$appPoolName["RapidFailProtection"]["FailureInterval"] = ($appPool.Failure.RapidFailProtectionInterval).ToString() + $appPoolDetails.$appPoolName["RapidFailProtection"]["MaximumFailures"] = $appPool.Failure.RapidFailProtectionMaxCrashes + $appPoolDetails.$appPoolName["RapidFailProtection"]["Executable"] = $appPool.Failure.AutoShutdownExe + $appPoolDetails.$appPoolName["RapidFailProtection"]["ExecutableParams"] = $appPool.Failure.AutoShutdownParams + + $appPoolDetails.$appPoolName["Recycling"] = New-Object System.Collections.Specialized.OrderedDictionary + $appPoolDetails.$appPoolName["Recycling"]["DisallowOverlappedRecycle"] = $appPool.Recycling.DisallowOverlappingRotation + $appPoolDetails.$appPoolName["Recycling"]["DisableConfigRecycling"] = $appPool.Recycling.DisallowRotationOnConfigChange + $appPoolDetails.$appPoolName["Recycling"]["RecyclingEventLogging"] = $appPool.Recycling.LogEventOnRecycle + $appPoolDetails.$appPoolName["Recycling"]["PrivateMemoryLimit"] = $appPool.Recycling.PeriodicRestart.PrivateMemory + $appPoolDetails.$appPoolName["Recycling"]["RegularTimeInterval"] = ($appPool.Recycling.PeriodicRestart.Time).ToString() + $appPoolDetails.$appPoolName["Recycling"]["RequestLimit"] = $appPool.Recycling.PeriodicRestart.Requests + $appPoolDetails.$appPoolName["Recycling"]["SpecificTimes"] = $appPool.Recycling.PeriodicRestart.Schedule + $appPoolDetails.$appPoolName["Recycling"]["VirtualMemoryLimit"] = $appPool.Recycling.PeriodicRestart.Memory + } + + foreach ($site in $mgr.Sites) { + + $siteName = $site.Name + + $webSiteDetails[$siteName] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName]["Id"] = $site.Id + $webSiteDetails[$siteName]["Name"] = $site.Name + $webSiteDetails[$siteName]["State"] = $site.State + $webSiteDetails[$siteName]["IsLocallyStored"] = $site.IsLocallyStored + $webSiteDetails[$siteName]["ServerAutoStart"] = $site.ServerAutoStart + $webSiteDetails[$siteName]["DefaultAppPool"] = $site.ApplicationDefaults.ApplicationPoolName + + $applicationHeader = "Applications" + $webSiteDetails[$siteName][$applicationHeader] = New-Object System.Collections.Specialized.OrderedDictionary + + foreach ($webApp in $site.Applications) { + + if ($webApp.Path -eq "/") { + + $webAppName = $webApp.Path + } + else { + + $webAppName = (($webApp.Path).Split("/") | Select-Object -Skip 1 -First 1) + } + + $webSiteDetails[$siteName][$applicationHeader][$webAppName] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["ApplicationPool"] = $webApp.ApplicationPoolName + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["EnabledProtocols"] = $webApp.EnabledProtocols + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["IsLocallyStored"] = $webApp.IsLocallyStored + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["PhysicalPath"] = $webApp.VirtualDirectories[0].PhysicalPath + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["Path"] = $webApp.Path + $webSiteDetails[$siteName][$applicationHeader][$webAppName]["PreloadEnabled"] = $webApp.Attributes["preloadEnabled"].Value + } + + $bindingHeader = "Bindings" + $webSiteDetails[$siteName][$bindingHeader] = New-Object System.Collections.Specialized.OrderedDictionary + + foreach ($binding in $site.Bindings) { + + $bindingInfo = $binding.BindingInformation + + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["HostName"] = $binding.Host + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["Protocol"] = $binding.Protocol + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["Address"] = $binding.Endpoint.Address.IPAddressToString + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["Port"] = $binding.Endpoint.Port + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["SslFlags"] = $binding.SslFlags + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["CertificateStoreName"] = $binding.CertificateStoreName + $webSiteDetails[$siteName][$bindingHeader][$bindingInfo]["CertificateHash"] = ` + if ($binding.CertificateHash) { + ([System.BitConverter]::ToString($binding.CertificateHash).Replace('-','')) + } + } + + $logHeader = "Log" + $webSiteDetails[$siteName][$logHeader] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName][$logHeader]["Enabled"] = $site.LogFile.Enabled + $webSiteDetails[$siteName][$logHeader]["Period"] = ($site.LogFile.Period).ToString() + $webSiteDetails[$siteName][$logHeader]["Rollover"] = $site.LogFile.LocalTimeRollover + $webSiteDetails[$siteName][$logHeader]["Format"] = $site.LogFile.LogFormat + $webSiteDetails[$siteName][$logHeader]["Directory"] = $site.LogFile.Directory + $webSiteDetails[$siteName][$logHeader]["Flags"] = ($site.LogFile.LogExtFileFlags).ToString() + + $reqLogHeader = "FailedRequestLog" + $webSiteDetails[$siteName][$reqLogHeader] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName][$reqLogHeader]["Enabled"] = $site.TraceFailedRequestsLogging.Enabled + $webSiteDetails[$siteName][$reqLogHeader]["MaxLogFiles"] = $site.TraceFailedRequestsLogging.MaxLogFiles + $webSiteDetails[$siteName][$reqLogHeader]["Directory"] = $site.TraceFailedRequestsLogging.Directory + $webSiteDetails[$siteName][$reqLogHeader]["MaxSizeKB"] = $site.TraceFailedRequestsLogging.Attributes["maxLogfileSizeKb"].Value + + $limitsHeader = "Limits" + $webSiteDetails[$siteName][$limitsHeader] = New-Object System.Collections.Specialized.OrderedDictionary + $webSiteDetails[$siteName][$limitsHeader]["ConnectionTimeout"] = ($site.Limits.ConnectionTimeout).ToString() + $webSiteDetails[$siteName][$limitsHeader]["MaxBandwidth"] = $site.Limits.MaxBandwidth + $webSiteDetails[$siteName][$limitsHeader]["MaxConnections"] = $site.Limits.MaxConnections + $webSiteDetails[$siteName][$limitsHeader]["MaxUrlSegments"] = $site.Limits.MaxUrlSegments + + } + } + catch { + + $iisDictionary["Error"] = $_.Exception.ToString() + } + + $iisDetails.Add("AppPools", $appPoolDetails) + $iisDetails.Add("Sites", $webSiteDetails) + + $iisDictionary.Add("IIS", $iisDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $iisDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-IISResetHistory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-IISResetHistory.ps1 new file mode 100644 index 0000000..6675596 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-IISResetHistory.ps1 @@ -0,0 +1,54 @@ +function Get-IISResetHistory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the IIS Reset History. +#> + + [CmdletBinding()] + Param( + $ID = 3201 + ) + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $resetDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $IISResetHistoryDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Event Log Entries for Event ID $ID" + $dateLimit = (Get-Date) - (New-TimeSpan -Day 90) + $resetEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'System' + Id = $ID + StartTime = $dateLimit + } + + foreach ($event in $resetEvents) { + + $eventId = [string]$event.RecordId + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Processing RecordId $eventId" + + $IISResetHistoryDetails[$eventId] = New-Object System.Collections.Specialized.OrderedDictionary + $IISResetHistoryDetails[$eventId]["Date"] = $event.TimeCreated; + $IISResetHistoryDetails[$eventId]["Process"] = $event.Properties[0].Value; + $IISResetHistoryDetails[$eventId]["Reason"] = $event.Properties[2].Value; + $IISResetHistoryDetails[$eventId]["Action"] = $event.Properties[4].Value; + $IISResetHistoryDetails[$eventId]["Comment"] = $event.Properties[5].Value; + $IISResetHistoryDetails[$eventId]["User"] = $event.Properties[6].Value; + $IISResetHistoryDetails[$eventId]["Message"] = $event.Message; + } + $IISResetHistoryDetails = ($IISResetHistoryDetails | sort-object )[0] + } catch { + + $IISResetHistoryDetails["Error"] = $_.Exception.ToString() + } + + $resetDictionary.Add("IISResetHistory", $IISResetHistoryDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $resetDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-LocalSecurityPolicyInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-LocalSecurityPolicyInventory.ps1 new file mode 100644 index 0000000..47c55c4 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-LocalSecurityPolicyInventory.ps1 @@ -0,0 +1,64 @@ +function Get-LocalSecurityPolicyInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Local Security Policy Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $userNameHasLogonAsServiceRights = @() + $userNameHasSystemProfileRights = @() + $localSecurityPolicyDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $localSecurityPolicyDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Logon As Service rights (SeServiceLogonRight)" + $SeServiceLogonAsServiceRightResult = Get-SecurityPolicySetting "SeServiceLogonRight" + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done reading Logon As Service rights (SeServiceLogonRight)" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading User Profile System Performance Rights (SeSystemProfilePrivilege)" + $SeSystemProfilePrivilegeResult = Get-SecurityPolicySetting "SeSystemProfilePrivilege" + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done reading User Profile System Performance Rights (SeSystemProfilePrivilege)" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Processing Policies" + + try { + + foreach ($sid in $SeServiceLogonAsServiceRightResult.Split(",")) { + $userNameHasLogonAsServiceRights += Get-UsernameFromSid ($sid.Replace("*", "")) + } + + if (Test-IsCollectionNullOrEmpty $userNameHasLogonAsServiceRights) { + Write-Verbose ("$logLead : [$($providerStopWatch.Elapsed)] : No users accounts found to have Logon as Service rights") + continue; + } + + foreach ($sid in $SeSystemProfilePrivilegeResult.Split(",")) { + $userNameHasSystemProfileRights += Get-UsernameFromSid ($sid.Replace("*", "")) + } + + if (Test-IsCollectionNullOrEmpty $userNameHasSystemProfileRights) { + Write-Verbose ("$logLead : [$($providerStopWatch.Elapsed)] : No users accounts found to have System Profile Privilege rights") + continue; + } + + $localSecurityPolicyDetails["localSecurityPolicyDetailsKey"] = New-Object System.Collections.Specialized.OrderedDictionary + $localSecurityPolicyDetails["localSecurityPolicyDetailsKey"]["userNameHasLogonAsServiceRights"] = $userNameHasLogonAsServiceRights + $localSecurityPolicyDetails["localSecurityPolicyDetailsKey"]["userNameHasSystemProfileRights"] = $userNameHasSystemProfileRights + + } + catch { + $localSecurityPolicyDetails["Error"] = $_.Exception.ToString() + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Processing Policies" + $localSecurityPolicyDictionary.Add("LocalSecurityPolices", $localSecurityPolicyDetails) + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + + $providerStopWatch.Stop() + + return $localSecurityPolicyDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-MachineInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-MachineInventory.ps1 new file mode 100644 index 0000000..54fd30a --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-MachineInventory.ps1 @@ -0,0 +1,245 @@ +function Get-MachineInventory { +<# +.SYNOPSIS + Returns an Object that Represents the State of a Machine + +.DESCRIPTION + Executes multiple collectors which describe the state of a machine. Can execute on an array of + machine names or PSSessions and allows for an optional filter to be included. May be returned as a PSObject, JSON, or XML + +.PARAMETER TargetComputerNames + [string[]] One or more remote computers to profile + +.PARAMETER PsSessions +[PSSession[]] One or more PSSessions to Use + +.PARAMETER Filter + [string[]] A filter which limits the number of profilers to execute + +.PARAMETER JsonOutput + [switch] Specifies to return the result as JSON + +.PARAMETER XmlOutput + [switch] Specifies to return the result as XML + +.INPUTS + None + +.OUTPUTS + An object with details about the machine state. May be a PSObject (default), JSON, or XML depending on arguments + +.EXAMPLE + Get-MachineInventory + +[Get-MachineInventory] : Collecting data from 1 target(s) +WARNING: [ALKA01VMA167] : Function Get-AppSettingsInventory Took 00:00:02.2702503 to Execute +[Get-MachineInventory] : Received 1 result(s) +[Get-MachineInventory] : Adding Results for ALKA01VMA167 +[Get-MachineInventory] : Inventory Collection Complete After 00:00:07.9261976 + +Name Value +---- ----- +ALKA01VMA167 {OS, Version, Architecture, GenerationTime...} + + +.EXAMPLE + Get-MachineInventory -Filter Uptime, FileSystem + +[Get-MachineInventory] : Collecting data from 1 target(s) +[Get-MachineInventory] : Received 1 result(s) +[Get-MachineInventory] : Adding Results for ALK-DELL1323 + +Name Value +---- ----- +ALK-DELL1323 {OS, Version, Architecture, GenerationTime...} + +.EXAMPLE + Get-MachineInventory -Filter Uptime -MachineNames @("localhost", "ALKA01VMA163.fh.local") + +[Get-MachineInventory] : Collecting data from 2 target(s) +[Get-MachineInventory] : Received 2 result(s) +[Get-MachineInventory] : Adding Results for ALKA01VMA163 +[Get-MachineInventory] : Adding Results for ALK-DELL1323 + +Name Value +---- ----- +ALKA01VMA163 {OS, Version, Architecture, GenerationTime...} +ALK-DELL1323 {OS, Version, Architecture, GenerationTime...} +#> + [CmdletBinding(DefaultParameterSetName = 'FilterOnlyParameterSet')] + [OutputType([string], ParameterSetName=("MachineNameParameterJson","SessionParameterJson","FilterParameterSetJson"))] + [OutputType([System.Xml.XmlDocument], ParameterSetName=("MachineNameParameterXml","SessionParameterXml","FilterParameterSetXml"))] + [OutputType([System.Collections.Specialized.OrderedDictionary], ParameterSetName=("MachineNameParameterSet","SessionParameterSet"))] + param( + + [Parameter(ParameterSetName = 'MachineNameParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'MachineNameParameterJson', Mandatory = $true)] + [Parameter(ParameterSetName = 'MachineNameParameterXml', Mandatory = $true)] + [Alias("ComputerNames", "Servers", "MachineNames")] + [string[]]$TargetComputerNames, + + [Parameter(ParameterSetName='SessionParameterSet', Mandatory=$true)] + [Parameter(ParameterSetName='SessionParameterJson', Mandatory=$true)] + [Parameter(ParameterSetName='SessionParameterXml', Mandatory=$true)] + [Alias("Sessions")] + [System.Management.Automation.Runspaces.PSSession[]]$PsSessions, + + [Parameter(ParameterSetName = 'MachineNameParameterJson', Mandatory = $true)] + [Parameter(ParameterSetName = 'SessionParameterJson', Mandatory = $true)] + [Parameter(ParameterSetName = 'FilterParameterSetJson', Mandatory = $false)] + [Alias("JsonOutput")] + [switch]$AsJson, + + [Parameter(ParameterSetName = 'MachineNameParameterXml', Mandatory = $true)] + [Parameter(ParameterSetName = 'SessionParameterXml', Mandatory = $true)] + [Parameter(ParameterSetName = 'FilterParameterSetXml', Mandatory = $false)] + [Alias("XMLOutput")] + [switch]$AsXML + ) + DynamicParam { + # Define the Paramater Attributes + $ParameterName = "Filter" + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $false + $ParameterAttribute.ParameterSetName = "__AllParameterSets" + $AttributeCollection.Add($ParameterAttribute) + + # Generate and add the ValidateSet + $arrSet = $inventoryFilterSetOptions | ForEach-Object { $_.FilterName } + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + + # Create the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string[]], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary + } + + begin { + # Bind the Filter dynamic parameter to a variable + $logLead = Get-LogLeadName + Write-Host "$logLead : Executing using ParameterSet: [$($PSCmdlet.ParameterSetName)]" + + $filterCollection = $PsBoundParameters[$ParameterName] + Write-Host "$logLead : Executing with filters: [$filterCollection]" + + # We'll Silently Continue for Any Provider Errors -- They Get Added to the Results + $originalErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "SilentlyContinue" + + $functionStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $inventoryResult = New-Object System.Collections.Specialized.OrderedDictionary + } + + process { + try { + $clearSessions = $true + if ($null -eq $PsSessions -or $PsSessions.Count -eq 0) { + $sessions = @() + foreach ($target in $TargetComputerNames) { + Write-Host "$logLead : Creating Session for $target" + $sessions += New-PSSession -ComputerName $target -ErrorVariable err -ErrorAction SilentlyContinue + + if($err.Count -gt 0) { + Write-Warning "$logLead : New-PSSession encountered an error : $err" + } + } + } else { + $clearSessions = $false + $sessions = $PsSessions + } + + $scriptBlock = { + + $logLead = "[$env:COMPUTERNAME]" + $ErrorActionPreference = "Continue" + + $machineData = New-Object System.Collections.Specialized.OrderedDictionary + $uniqueSections = $Using:inventoryFilterSetOptions | ForEach-Object { $_.SectionVariable } | Sort-Object | Select-Object -Unique + + $VerbosePreference=$Using:VerbosePreference; + + foreach ($section in $uniqueSections) { + Write-Verbose "$logLead : Creating Variable for $section Data" + New-Variable -Name $section -Value (New-Object System.Collections.Specialized.OrderedDictionary) + } + + # Always include Platform Inventory as Top Level Properties + $machineData += Get-PlatformInventory + + # Use the entire filter set if no filter was set as a parameter + [array]$filteredInventories = Test-IsNull $Using:filterCollection ($Using:inventoryFilterSetOptions | ForEach-Object { $_.FilterName }) -Strict + Write-Verbose "$logLead : Using Final Inventory Filter: [$filteredInventories]" + + foreach ($inventory in ($filteredInventories | Sort-Object)) { + + $matchingFilter = $Using:inventoryFilterSetOptions | Where-Object {$_.FilterName -eq $inventory} + + $masterProviderStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + Write-Host "$logLead : Executing FunctionName [$($matchingFilter.FunctionName)]" + $sb = [ScriptBlock]::Create($matchingFilter.FunctionName) + ((Get-Variable -Name $matchingFilter.SectionVariable).Value) += (Invoke-Command $sb) + + $masterProviderStopWatch.Stop() + + if ($masterProviderStopWatch.ElapsedMilliseconds -ge 1000) { + + Write-Warning "$logLead : Function $($matchingFilter.FunctionName) Took $($masterProviderStopWatch.Elapsed) to Execute" + } + } + + foreach ($uniqueSection in $uniqueSections) { + $section = (Get-Variable -Name $uniqueSection -ValueOnly) + if ($section.Count -gt 0) { + + Write-Verbose "$logLead : Adding $uniqueSection Section to Results" + $machineData.Add("$uniqueSection", $section) + } + } + + return $machineData + } + + Write-Host "$logLead : Collecting data from $($sessions.Count) target(s)" + [array]$results = Invoke-Command -ScriptBlock $scriptBlock -Session $sessions + + Write-Host "$logLead : Received $($results.Count) result(s)" + + # Add Result Sets + foreach ($result in $results) { + Write-Host "$logLead : Adding Results for $($result.Hostname)" + $inventoryResult.Add($result.Hostname, $result) + } + } finally { + Write-Host "$logLead : Setting ErrorActionPreference to $originalErrorActionPreference" + $ErrorActionPreference = $originalErrorActionPreference + + if ($clearSessions) { + if ($sessions.Count -gt 0) { + Write-Host "$logLead : Cleaning Up PSSessions" + Remove-PSSession $sessions; + } else { + Write-Error "$logLead : WinRM cannot connect to any computers in the input list." + } + } + } + + $functionStopWatch.Stop() + Write-Host "$logLead : Inventory Collection Complete After $($functionStopWatch.Elapsed)" + + if ($AsJson) { + return ($inventoryResult | ConvertTo-Json -Depth 8) + } + + if ($AsXml) { + return ($inventoryResult | ConvertTo-Xml -Depth 8 -As Document) + } + + return $inventoryResult + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-MemoryInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-MemoryInventory.ps1 new file mode 100644 index 0000000..c5cce8a --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-MemoryInventory.ps1 @@ -0,0 +1,43 @@ +function Get-MemoryInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Memory Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $memoryDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $memoryDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Set-Variable megaByteDivisor -Option Constant -Value 1048576 + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Memory Details" + $memory = Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_PerfRawData_PerfOS_Memory ` + -Property CommitLimit, AvailableMBytes, CommittedBytes, CacheBytes, PoolNonpagedBytes, PoolPagedBytes + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Memory Details Retrieved" + + $memoryDetails["TotalMBytes"] = ($memory.CommitLimit / $megaByteDivisor) + $memoryDetails["AvailableMBytes"] = $memory.AvailableMBytes + $memoryDetails["CommittedMBytes"] = [int]($memory.CommittedBytes / $megaByteDivisor) + $memoryDetails["SystemCacheMBytes"] = [int]($memory.CacheBytes / $megaByteDivisor) + $memoryDetails["NonPagedPoolMBytes"] = [int]($memory.PoolNonpagedBytes / $megaByteDivisor) + $memoryDetails["PagedPoolMBytes"] = [int]($memory.PoolPagedBytes / $megaByteDivisor) + + $memoryDictionary.Add("Memory" , $memoryDetails) + } + catch { + + $memoryDetails["Error"] = $_.Exception.ToString() + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $memoryDictionary +} diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ModuleInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ModuleInventory.ps1 new file mode 100644 index 0000000..4822dce --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ModuleInventory.ps1 @@ -0,0 +1,44 @@ +function Get-ModuleInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Module Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $moduleDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $moduleDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Available Modules" + $localModules = Get-Module -ListAvailable -Verbose:$false | Select-Object ModuleType, Name, Version, ExportedCommands, ModuleBase, Description + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Module Inventory Complete" + + foreach ($module in $localModules) { + + $moduleDetails[$module.Name] = New-Object System.Collections.Specialized.OrderedDictionary + $moduleDetails[$module.Name]["ModuleType"] = $module.ModuleType + $moduleDetails[$module.Name]["Name"] = $module.Name + $moduleDetails[$module.Name]["Version"] = $module.Version + $moduleDetails[$module.Name]["ExportedCommands"] = ($module.ExportedCommands.Values | Select-Object -ExpandProperty Name) + $moduleDetails[$module.Name]["ModuleBase"] = $module.ModuleBase + $moduleDetails[$module.Name]["Description"] = $module.Description + } + } + catch { + + $moduleDetails["Error"] = $_.Exception.ToString() + } + + $moduleDictionary.Add("Modules", $moduleDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $moduleDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-OrbInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-OrbInventory.ps1 new file mode 100644 index 0000000..1f86009 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-OrbInventory.ps1 @@ -0,0 +1,40 @@ +function Get-OrbInventory { +<# +.SYNOPSIS + Returns a hash table that Represents the ORB Inventory. +#> + + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Orb Version Details" + $orbVersion = Get-OrbVersion + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Orb Version Details Retrieved" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting ServerType Details" + $IsWebServer = Test-IsWebServer + $IsAppServer = Test-IsAppServer + $IsMicServer = Test-IsMicroServer + $IsFabServer = Test-IsServiceFabricServer + $IsAWSServer = Test-IsAws + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : ServerType Details Retrieved" + + + $orbInfo = @{ + orbVersion = $orbVersion; + isWebServer = $IsWebServer; + isAppServer = $IsAppServer; + isMicServer = $IsMicServer; + isFabServer = $IsFabServer; + isAws = $IsAWSServer; + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $orbInfo +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformElementInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformElementInventory.ps1 new file mode 100644 index 0000000..43109f4 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformElementInventory.ps1 @@ -0,0 +1,48 @@ +function Get-PlatformElementInventory { + <# + .SYNOPSIS + Get all PlatformElementDetail data in json from a BoRG Uri based on the EnvironmentName + + .EXAMPLE + Get-PlatformElementInventory -BorgUri "http://uri.to.borg.com" -EnvironmentName "AWS Sandbox Box 0.2 MIC" + + .PARAMETER BorgUri + The Uri to the BoRG REST endpoint + + .PARAMETER EnvironmentName + Filters based on the provided Environment Name. If not provided, the local machine.config Environment.Name appSetting value will be used. + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $false)] + [string]$EnvironmentName + ) + + $packages = New-Object System.Collections.ArrayList + + #Get environment name locally if not provided + if ([String]::IsNullOrEmpty($environmentName)) { + $environmentName = Get-AppSetting -appSettingKey "Environment.Name" + } + + $Uri = "$BorgUri/odata/PlatformElementDetails?`$filter=IsCurrent eq true and EnvironmentType/Name eq '$environmentName'&`$expand=Element(`$select=Name;`$expand=ElementTier(`$select=Name)),ElementVersion(`$select=Name)&`$select=Element,ElementVersion" + + $platformElementInventory = Invoke-RestMethod -Uri $Uri -Method GET -ContentType "application/json" + + if ($platformElementInventory.value.Length -gt 0) { + foreach ($item in $platformElementInventory.value) { + $name = $item.Element.Name + $version = $item.ElementVersion.Name + $tier = $item.Element.ElementTier.Name + $properties = @{ Name = $name; Version = $version; Tier = $tier; Feed = $null; Tags = $null; IsService = $null; StartMode = $null } + $pkg = New-Object -TypeName PSObject -Prop $properties + ($packages.Add($pkg)) | Out-Null + } + } + + return $packages +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformInventory.ps1 new file mode 100644 index 0000000..3bd7477 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-PlatformInventory.ps1 @@ -0,0 +1,51 @@ +function Get-PlatformInventory { +<# +.SYNOPSIS + Get information about the Platform + +.LINK + Get-MachineInventory +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Getting OS Details" + $platformInfo = Get-CIMInstance Win32_OperatingSystem -Namespace "root\CIMV2" -Property Caption, Version, OSArchitecture, Organization, SizeStoredInPagingFiles + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : OS Details Retrieved" + + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Getting System Details" + $systemInfo = Get-CIMInstance Win32_ComputerSystem -Namespace "root\CIMV2" -Property DNSHostName, Domain + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : System Details Retrieved" + + $psVersionInfo = $PSVersionTable.PSVersion + + $platformInfo = @{ + GenerationTime = (Get-Date).DateTime; + OS = "Windows"; + PlatformFamily = "Windows"; + Platform = $platformInfo.Caption; + Version = $platformInfo.Version; + Architecture = $platformInfo.OSArchitecture; + Organization = $platformInfo.Organization; + PageFileSize = $platformInfo.SizeStoredInPagingFiles; + Hostname = $systemInfo.DNSHostName; + DotNetVersion = (Get-DotNetVersion).FriendlyVersion; + FQDN = (Get-FullyQualifiedServerName); + PowerShell = @{ + Version = $psVersionInfo.ToString(); + Major = $psVersionInfo.Major; + Minor = $psVersionInfo.Minor; + Build = $psVersionInfo.Build; + Revision = $psVersionInfo.Revision; + } + } + + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $platformInfo +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ProcessorInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ProcessorInventory.ps1 new file mode 100644 index 0000000..e630c18 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ProcessorInventory.ps1 @@ -0,0 +1,60 @@ +function Get-ProcessorInventory { +<# +.SYNOPSIS + Get information about the CPU(s) + +.LINK + Get-MachineInventory +#> + [CmdletBinding()] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + Param() + + $logLead = Get-LogLeadName + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $cpuDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $cpuDetails = New-Object System.Collections.Specialized.OrderedDictionary + $cpuIndex = 0 + $totalCoreCount = 0 + + try { + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Getting Processor Details" + $processors = Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_Processor ` + -Property Manufacturer, Family, Revision, NumberOfLogicalProcessors, Description, MaxClockSpeed + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Processor Details Retrieved" + + foreach ($processor in $processors) { + + $indexId = Test-IsNull $processor.DeviceId ([string]$cpuIndex) -Strict + + $coreCount = $processor.NumberOfCores + $totalCoreCount += $coreCount + + $cpuDetails[$indexId] = New-Object System.Collections.Specialized.OrderedDictionary + $cpuDetails[$indexId]["Manufacturer"] = $processor.Manufacturer + $cpuDetails[$indexId]["Family"] = [string]$processor.Family + $cpuDetails[$indexId]["Revision"] = [string]$processor.Revision + $cpuDetails[$indexId]["CoreCount"] = $coreCount + $cpuDetails[$indexId]["LogicalCores"] = $processor.NumberOfLogicalProcessors + $cpuDetails[$indexId]["Description"] = $processor.Description + $cpuDetails[$indexId]["ClockSpeed"] = [string]$processor.MaxClockSpeed + + $cpuIndex += 1 + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : $indexId Iteration Complete" + } + + $cpuDetails["Sockets"] = $cpuIndex + $cpuDetails["PhysicalCores"] = Test-IsNull $totalCoreCount $null -Strict + } + catch { + $cpuDetails["Error"] = $_.Exception.ToString() + } + + $cpuDictionary.Add("Processor" , $cpuDetails) + + Write-Host "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $cpuDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-RestartHistory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-RestartHistory.ps1 new file mode 100644 index 0000000..b9f6f75 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-RestartHistory.ps1 @@ -0,0 +1,52 @@ +function Get-RestartHistory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Restart History. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $historyDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $rebootHistoryDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Event Log Entries for Event ID 1074" + $dateLimit = (Get-Date) - (New-TimeSpan -Day 90) + $restartEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'System' + Id = 1074 + StartTime = $dateLimit + } + + foreach ($event in $restartEvents) { + + $eventId = [string]$event.RecordId + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Processing RecordId $eventId" + + $rebootHistoryDetails[$eventId] = New-Object System.Collections.Specialized.OrderedDictionary + $rebootHistoryDetails[$eventId]["Date"] = $event.TimeCreated; + $rebootHistoryDetails[$eventId]["Process"] = $event.Properties[0].Value; + $rebootHistoryDetails[$eventId]["Reason"] = $event.Properties[2].Value; + $rebootHistoryDetails[$eventId]["Action"] = $event.Properties[4].Value; + $rebootHistoryDetails[$eventId]["Comment"] = $event.Properties[5].Value; + $rebootHistoryDetails[$eventId]["User"] = $event.Properties[6].Value; + $rebootHistoryDetails[$eventId]["Message"] = $event.Message; + } + } + catch { + + $rebootHistoryDetails["Error"] = $_.Exception.ToString() + } + + $historyDictionary.Add("RestartHistory", $rebootHistoryDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $historyDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-ServiceFabricInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-ServiceFabricInventory.ps1 new file mode 100644 index 0000000..2aebcb2 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-ServiceFabricInventory.ps1 @@ -0,0 +1,67 @@ +function Get-ServiceFabricInventory { + <# + .SYNOPSIS + Get a PSObject containing categorized packages which represents the assumed list of what is installed on a Fab machine + + .EXAMPLE + Get-ServiceFabricInventory + + .PARAMETER ComputerName + The name of the computer to inventory + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $SFDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $SFDetails = New-Object System.Collections.Specialized.OrderedDictionary + $serviceFabricPackages = New-Object System.Collections.Specialized.OrderedDictionary + $requiredServiceFabricPackages = New-Object System.Collections.Specialized.OrderedDictionary + + if (!(Test-IsServiceFabricServer)) { + Write-Warning "$logLead : Not a service fabric node. Inventory collection will be skipped" + $SFDictionary.Add("ServiceFabric", $null ) + return $SFDictionary + } + + try { + Import-Module serviceFabric + } catch { + Write-Warning "$logLead : $($_.exception.message)" + $SFDictionary.Add("ServiceFabric", $null ) + return $SFDictionary + } + + Connect-AlkamiServiceFabricCluster -hostname $ComputerName | Out-Null + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Service Fabric Packages" + $localServiceFabricPackages = (Get-AlkamiServiceFabricApplications); + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Service Fabric Packages" + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Determing environment name" + $environmentName = Get-AppSetting "Environment.Name" + $envName = (Format-AlkamiEnvironmentName -name $environmentName); + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done determing environment name" + + # Filter the packages down to the specific environment, and then record the packages. + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Filtering packages" + $environmentPackages = $localServiceFabricPackages | Where-Object { ($null -eq $_.Environment) -or ($_.Environment -eq $envName)} + foreach ($package in $environmentPackages) { + $serviceFabricPackages.Add("$($package.ServiceFabricApplicationName)", $package); + } + + $SFDetails.Add("requiredServiceFabricPackages", $requiredServiceFabricPackages) + $SFDetails.Add("Packages", $serviceFabricPackages) + $SFDictionary.Add("ServiceFabric", $SFDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $SFDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-SystemWebSettingsInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-SystemWebSettingsInventory.ps1 new file mode 100644 index 0000000..a173d76 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-SystemWebSettingsInventory.ps1 @@ -0,0 +1,52 @@ +function Get-SystemWebSettingsInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the System Web Settings Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + $systemWebSettingsDetails = @{ + httpRuntime = @{}; + processModel = @{} + machineKey = @{}; + } + + + $desiredSystemWebNodes = @{ + "httpRuntime" = @{"minFreeThreads" = 0; "minLocalRequestFreeThreads" = 0}; + "processModel" = @{"autoConfig" = ""; "maxWorkerThreads" = 0; "maxIoThreads" = 0; "minWorkerThreads" = 0; "minIoThreads" = 0}; + "machineKey" = @{"validationKey" = ""; "decryptionKey" = ""; "decryption" = ""}; + } + + $systemWebSettingsDictionary = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Machine Config Data" + + [xml]$machineConfigRaw = Read-MachineConfig + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Machine Config Data Retrieved" + + try { + + foreach ($nodeKey in $desiredSystemWebNodes.keys) { + $tempXmlNode = ([xml]$machineConfigRaw).SelectNodes("//system.web/$nodeKey") + foreach ($keyname in $desiredSystemWebNodes.$nodeKey.keys) { + $systemWebSettingsDetails.$nodeKey[$keyname] = $tempXmlNode.$keyname + } + } + } catch { + + $systemWebSettingsDetails["Error"] = $_.Exception.ToString() + } + + $systemWebSettingsDictionary.Add("systemWebSettings", $systemWebSettingsDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $systemWebSettingsDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-TimeConfiguration.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-TimeConfiguration.ps1 new file mode 100644 index 0000000..7bb9f92 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-TimeConfiguration.ps1 @@ -0,0 +1,43 @@ +function Get-TimeConfiguration { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Time Configuration. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $timeSettingDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $timeDetails = New-Object System.Collections.Specialized.OrderedDictionary + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Getting Time Configuration Details" + + try { + + $dateTime = Get-Date + $timeZone = Get-TimeZone + + $timeDetails = New-Object PSObject -Property @{ + "CurrentTime" = $dateTime.ToLongTimeString() + "CurrentDate" = $dateTime.Date + "CurrentTicks" = $dateTime.Ticks + "TimeZoneId" = $timeZone.Id + "TimeZoneName" = $timeZone.StandardName + "BaseUtcOffset" = $timeZone.BaseUtcOffset + } + } + catch { + + $timeDetails["Error"] = $_.Exception.Message + } + + $timeSettingDictionary.Add("TimeConfiguration", $timeDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $timeSettingDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsFeatureInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsFeatureInventory.ps1 new file mode 100644 index 0000000..cd9f9a8 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsFeatureInventory.ps1 @@ -0,0 +1,53 @@ +function Get-WindowsFeatureInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Windows Feature Inventory. +#> + + [CmdletBinding()] + param() + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $featureDictionary = New-Object System.Collections.Specialized.OrderedDictionary + $featureDetails = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + if ((Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_OperatingSystem -Property Caption).Caption -notmatch "Server") { + + $logError = "Windows Feature Inventory Not Supported on Client Operating Systems" + Write-Warning "$logLead : $logError" + throw $logError + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Windows Feature and Role Details" + + Import-Module ServerManager -Verbose:$false | Out-Null + $features = Get-WindowsFeature | Where-Object {$_.Installed} + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Windows Feature and Role Data Retrieved" + + foreach ($feature in $features) { + + $featureDetails[$feature.Name] = New-Object System.Collections.Specialized.OrderedDictionary + $featureDetails[$feature.Name]["Name"] = $feature.Name + $featureDetails[$feature.Name]["DisplayName"] = $feature.DisplayName + $featureDetails[$feature.Name]["FeatureType"] = $feature.FeatureType + $featureDetails[$feature.Name]["Path"] = $feature.Path + $featureDetails[$feature.Name]["Depth"] = $feature.Depth + $featureDetails[$feature.Name]["Parent"] = $feature.Parent + } + } + catch { + + $featureDetails["Error"] = $_.Exception.ToString() + } + + $featureDictionary.Add("Features", $featureDetails) + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $featureDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsServiceInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsServiceInventory.ps1 new file mode 100644 index 0000000..ab81ea0 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Get-WindowsServiceInventory.ps1 @@ -0,0 +1,34 @@ +function Get-WindowsServiceInventory { +<# +.SYNOPSIS + Returns an OrderedDictionary that Represents the Windows Service Inventory. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + $serviceDictionary = New-Object System.Collections.Specialized.OrderedDictionary + + try { + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Windows Service Information" + $services = Get-CIMInstance -Namespace "root\CIMV2" -Class Win32_Service ` + -Property Name, ProcessId, State, StartMode, Status, PathName, StartName, Description, DisplayName ` + | Select-Object -Property Name, ProcessId, State, StartMode, Status, PathName, StartName, Description, DisplayName + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Windows Service Information Retrieved" + + $serviceDictionary.Add("Services", $services) + } + catch { + + $serviceDictionary["Error"] = $_.Exception.ToString() + } + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Provider Complete" + $providerStopWatch.Stop() + + return $serviceDictionary +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetailPSObject.ps1 b/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetailPSObject.ps1 new file mode 100644 index 0000000..1107347 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetailPSObject.ps1 @@ -0,0 +1,55 @@ +function New-PlatformElementDetailPSObject { + <# + .SYNOPSIS + Convenience function to create and return a PSOBject required by the BoRG REST API for PlatformElementDetails updates + + .EXAMPLE + $detailObj = New-PlatformElementDetailPSObject ` + -EnvironmentTypeName "AWS Sandbox 0.2" ` + -PlatformVersionName "R2019.07.1.731" ` + -ElementTierName "Web" ` + -ElementName "Alkami.Ops.Common" ` + -ElementVersionName "3.0.3" + + .PARAMETER EnvironmentTypeName + The Environment label + + .PARAMETER PlatformVersionName + The Platform version value + + .PARAMETER ElementTierName + The label of the tier (Web, App, Fab, Mic, etc.) + + .PARAMETER ElementName + The name of the actual element + + .PARAMETER ElementVersion + The version of the actual element + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$EnvironmentTypeName, + + [Parameter(Mandatory = $false)] + [string]$PlatformVersionName, + + [Parameter(Mandatory = $false)] + [string]$ElementTierName, + + [Parameter(Mandatory = $true)] + [string]$ElementName, + + [Parameter(Mandatory = $false)] + [string]$ElementVersionName + ) + + return New-Object PSObject -Property @{ + "EnvironmentTypeName" = $EnvironmentTypeName + "PlatformVersionName" = $PlatformVersionName + "ElementName" = $ElementName + "ElementTierName" = $ElementTierName + "ElementVersionName" = $ElementVersionName + "DoNotCopy" = $false + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetails.ps1 b/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetails.ps1 new file mode 100644 index 0000000..f282da2 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/New-PlatformElementDetails.ps1 @@ -0,0 +1,40 @@ +function New-PlatformElementDetails { + <# + .SYNOPSIS + Use to persist an array of PlatformElementDetails to BoRG + + .EXAMPLE + New-PlatformElementDetails -PlatformElementDetailArray @($obj) -BorgUri "http://uri.to.borg.com" + + .PARAMETER PlatformElementDetailArray + An array of PSObjects containing all element details to be persisted. Use New-PlatformElementDetailPSObject for creation of the required PSObjects. + + .PARAMETER BorgUri + Uri to the BoRG REST service. + + .PARAMETER ApiKey + Borg Api key. Required for write operations. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [PSObject[]]$PlatformElementDetailArray, #Use New-PlatformElementDetailPSObject to create each PSObject + + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $true)] + [string]$ApiKey + ) + + $uri = "$BorgUri/api/PlatformElementDetails/PostCascadeChildren" + $headers = @{ "x-api-key" = $ApiKey } + $body = ConvertTo-Json $PlatformElementDetailArray + + $response = Invoke-CommandWithRetry -Arguments @($uri,$headers,$body) -Seconds 3 -JitterMin 500 -JitterMax 3000 -ScriptBlock { + param($uri,$headers,$body) + return (Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -body $body -ContentType "application/json") + } + + return ConvertTo-Json $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetailById.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetailById.ps1 new file mode 100644 index 0000000..3684922 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetailById.ps1 @@ -0,0 +1,40 @@ +function Remove-PlatformElementDetailById { + <# + .SYNOPSIS + Soft deletes a PlatformElementDetail by Id in BoRG by setting IsCurrent = false + + .EXAMPLE + Remove-PlatformElementDetailById -PlatformElementDetailId 1 -BorgUri "http://uri.to.borg.com" -APiKey "b752be2f-9206-4706-a2fb-851162cb29bf" + + .PARAMETER PlatformElementDetailId + The id of the element details to be removed. + + .PARAMETER BorgUri + Uri to the BoRG REST service. + + .PARAMETER ApiKey + Borg Api key. Required for write operations. + #> + [CmdletBinding()] + [OutputType([string])] + Param( + [Parameter(Mandatory = $true)] + [long]$PlatformElementDetailId, + + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $true)] + [string]$ApiKey + ) + + $uri = "$BorgUri/api/platformelementdetails/$PlatformElementDetailId" + $headers = @{ "x-api-key" = $ApiKey } + + $response = Invoke-CommandWithRetry -Arguments @($uri, $headers) -Seconds 3 -JitterMin 500 -JitterMax 3000 -ScriptBlock { + param($uri, $headers) + return (Invoke-RestMethod -Uri $uri -Method PATCH -Headers $headers -ContentType "application/json") + } + + return ConvertTo-Json $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetails.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetails.ps1 new file mode 100644 index 0000000..8133ba2 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Remove-PlatformElementDetails.ps1 @@ -0,0 +1,41 @@ +function Remove-PlatformElementDetails { + <# + .SYNOPSIS + Soft deletes PlatformElementDetail(s) in BoRG by setting IsCurrent = false + + .EXAMPLE + Remove-PlatformElementDetails -PlatformElementDetailArray @(item1, item2) -BorgUri "http://uri.to.borg.com" -APiKey "b752be2f-9206-4706-a2fb-851162cb29bf" + + .PARAMETER PlatformElementDetailArray + An array of PSObjects containing all element details to be deleted. Use New-PlatformElementDetailPSObject for creation of the required PSObjects. + + .PARAMETER BorgUri + Uri to the BoRG REST service. + + .PARAMETER ApiKey + Borg Api key. Required for write operations. + #> + [CmdletBinding()] + [OutputType([string])] + Param( + [Parameter(Mandatory = $true)] + [PSObject[]]$PlatformElementDetailArray, + + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $true)] + [string]$ApiKey + ) + + $uri = "$BorgUri/api/platformelementdetails" + $headers = @{ "x-api-key" = $ApiKey } + $body = ConvertTo-Json $PlatformElementDetailArray + + $response = Invoke-CommandWithRetry -Arguments @($uri, $headers, $body) -Seconds 3 -JitterMin 500 -JitterMax 3000 -ScriptBlock { + param($uri, $headers, $body) + return (Invoke-RestMethod -Uri $uri -Method PATCH -Headers $headers -Body $body -ContentType "application/json") + } + + return ConvertTo-Json $response +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Save-MachineManifest.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Save-MachineManifest.ps1 new file mode 100644 index 0000000..144a4a1 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Save-MachineManifest.ps1 @@ -0,0 +1,33 @@ +function Save-MachineManifest { +<# + .SYNOPSIS + Saves a json manifest to the specified location. Defaults to c:\temp\manifest if not supplied + + .EXAMPLE + Save-MachineManifest + + This will save a manifest for the local machine at c:\temp\manifest + + Save-MachineManifest -Folder "c:\temp\foo" + + This will save a manifest of the local machine to "c:\temp\foo" + + .PARAMETER outputFolder + Optional output location +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias("Folder")] + [string]$outputFolder = "c:\temp\manifest" + ) + + if(!(Test-Path $outputFolder)) + { + New-Item -Path $defaultTempFileLocation -ItemType "directory" + } + + $filename = "$outputFolder\$env:computername.json" + Get-MachineInventory -filter chocolatey -asJson | Out-File -FilePath $filename +} diff --git a/Modules/Alkami.DevOps.Inventory/Public/Send-DeploymentManifestToS3.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Send-DeploymentManifestToS3.ps1 new file mode 100644 index 0000000..dcb818d --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Send-DeploymentManifestToS3.ps1 @@ -0,0 +1,112 @@ +function Send-DeploymentManifestToS3 { +<# + .SYNOPSIS + Generates an inventory for the current machine (or accepts one from an existing local file location) and uploads it to s3. If a valid bucket is supplied that will be targeted. Otherwise a bucket name will be generated from machine.config values. If that generates an invalid bucket name it will throw. + + .EXAMPLE + Send-DeploymentManifestToS3 + + This will upload a local manifest to the bucket generated based on machine.config values. + + Send-DeploymentManifestToS3 -Bucket "alkami-manifest-test" + + This will upload a local manifest to the "alkami-manifest-test" bucket + + Send-DeploymentManifestToS3 -Bucket "alkami-manifest-test" -Filename "C:\Temp\foo1.json" + + This will upload the manifest in the specified file (no validation is performed here) to the "alkami-manifest-test" bucket + + .PARAMETER S3BucketName + Optional bucket name. Mostly used for local (non-aws) machine testing. + + .PARAMETER InventoryFilename + Optional local manifest file name. + .PARAMETER ProfileName + [string] Specific AWS CLI Profile to use in AWS API calls +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias("Bucket")] + [string]$S3BucketName, + + [Parameter(Mandatory = $false)] + [Alias("Filename")] + [string]$InventoryFilename, + + [Parameter(Mandatory = $false)] + [string]$ProfileName + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule # S3 + + $defaultTempFileLocation = "c:\temp\manifest" + $filename = "" + $splatParams = @{} + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $splatParams["ProfileName"] = "$ProfileName" + } + + if ([string]::IsNullOrEmpty($s3BucketName)) { + $envName = Get-AppSetting "Environment.Name" + $envType = Get-AppSetting "Environment.Type" + + $envName = $envName.Split(" ")[-1].Replace(".", "-") + + if ($envType -eq "Production") { + $envType = "prod" + } + + $s3BucketName = "orb-$envType-$envName-script-bucket" + } + + if ([string]::IsNullOrEmpty($inventoryFilename)) { + if (!(Test-Path $defaultTempFileLocation)) { + New-Item -Path $defaultTempFileLocation -ItemType "directory" + } + $filename = "$defaultTempFileLocation\$env:computername.json" + Get-MachineInventory -filter chocolatey -asJson | Out-File -FilePath $filename + } + elseif (!($null -eq $inventoryFilename)) { + $filename = $inventoryFilename + } + else { + throw "$logLead : Inventory source is required" + } + + $leafName = Split-Path $filename -leaf + + $leafNameWithoutExtension = $leafName.Split(".")[0] + + $existingManifests = Get-S3Object -BucketName $s3BucketName -KeyPrefix $leafNameWithoutExtension @splatParams + + if ($null -eq $existingManifests) { + Write-S3Object -BucketName $s3BucketName -File $filename @splatParams + } + else { + + if ($existingManifests.Count -gt 1) { + $oldManifest = $existingManifests | Where-Object {$_.Key -ne $leafName} + + if ($oldManifest.Count -gt 1) { + throw "$loglead : Unexpected manifests found in S3. Please manually inspect the bucket." + } + + # Force this delete so that we don't ask for human input. + Remove-S3Object -BucketName $s3BucketName -Key $oldManifest.Key -Force @splatParams + } + + $fileTimestamp = Get-Date -Format FileDateTimeUniversal + $destinationKey = [System.Io.Path]::GetFileNameWithoutExtension($leafName) + "-" + $fileTimestamp + ".json" + + Copy-S3Object -BucketName $s3BucketName -key $leafName -DestinationBucket $s3BucketName -DestinationKey $destinationKey @splatParams + + # This should overwrite the existing one that we just copied to a new renamed file. + Write-S3Object -BucketName $s3BucketName -File $filename @splatParams + } + Write-Host "Manifest has been uploaded to $s3BucketName." +} diff --git a/Modules/Alkami.DevOps.Inventory/Public/Sync-PlatformElementInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Sync-PlatformElementInventory.ps1 new file mode 100644 index 0000000..c42fc8c --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Sync-PlatformElementInventory.ps1 @@ -0,0 +1,85 @@ +function Sync-PlatformElementInventory { + <# + .SYNOPSIS + Fully synchronizes the element inventory of an Environment to BoRG. The final BoRG contents will + match the environment contents. Useful for populating BoRG with the element list of a new environment or + correcting major deviations. + + WARNING: This function persists ALL discovered elements in an environment, regardless of tier. Elements + installed on an incorrect tier can result in a failed MDV during deployments. + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [string[]]$Servers, + + [Parameter(Mandatory = $true)] + [string]$EnvironmentName, + + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $true)] + [string]$ApiKey + ) + + Write-Host "Starting BoRG full sync for $EnvironmentName..." + + #Pull current BoRG Inventory + try { + Write-Host "Retrieving BoRG platform element inventory for $EnvironmentName." + $borgInventory = (Get-PlatformElementInventory -BorgUri $BorgUri -EnvironmentName $EnvironmentName) + } catch { + Write-Error "An error occurred Retrieving BoRG inventory: $($_.Exception.Message)" + } + + #Pull machine inventory + Write-Host "Retrieving machine inventory for $($Servers -join ',')." + $machineInventory = (Get-MachineInventory -ComputerNames $Servers -Filter Chocolatey,ServiceFabric) + + #Compare contents of Machines against BoRG and build add/remove collections + $borgElementsToAdd = @{} + $borgElementsToRemove = @{} + + foreach ($key in $machineInventory.Keys) { + #Get element list from inventory + $machineTier = $key.SubString(0,3).ToLower() + if ($machineTier -eq "fab") { + $machineElements = $machineInventory.$key.ConfigData.ServiceFabric.Packages + } else { + $machineElements = $machineInventory.$key.ConfigData.Chocolatey.Packages + } + + Write-Host "Determining elements to add/update for $key." + + foreach ($element in $machineElements.Values) { + if ($element.SourceName -match "Alkami|SDK" -and $element.Name -notmatch "PowerShell" -and !$borgElementsToAdd.ContainsKey($element.Name)) { + $borgElementsToAdd[$element.Name] = $element + } + } + } + + Write-Host "Determining elements to remove from BoRG." + foreach ($element in $borgInventory) { + if (!$borgElementsToAdd.ContainsKey($element.Name)) { + $borgElementsToRemove[$element.Name] = $element + } + } + + #Update BoRG + Write-Host "Updating BoRG [$BorgUri] Inventory." + try { + Update-PlatformElementInventory ` + -ElementsToAdd @($borgElementsToAdd.Values) ` + -ElementsToRemove @($borgElementsToRemove.Values) ` + -EnvironmentTypeName $EnvironmentName ` + -PlatformVersionName "Unspecified" ` + -ElementTierName "Unknown" ` + -BorgUri $BorgUri ` + -ApiKey $ApiKey + } catch { + Write-Error "An exception occurred while updating BoRG. Error: $($_.Exception.Message)" + } + + Write-Host "BoRG full sync for $EnvironmentName completed successfully." +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/Public/Update-PlatformElementInventory.ps1 b/Modules/Alkami.DevOps.Inventory/Public/Update-PlatformElementInventory.ps1 new file mode 100644 index 0000000..6eaf9f9 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/Public/Update-PlatformElementInventory.ps1 @@ -0,0 +1,72 @@ +function Update-PlatformElementInventory { +<# +.SYNOPSIS + Adds or Removes the Element name/versions in BoRG to reflect changes. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [AllowNull()] + [object[]]$ElementsToAdd, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [object[]]$ElementsToRemove, + + [Parameter(Mandatory = $true)] + [string]$EnvironmentTypeName, + + [Parameter(Mandatory = $true)] + [string]$PlatformVersionName, + + [Parameter(Mandatory = $true)] + [string]$ElementTierName, + + [Parameter(Mandatory = $true)] + [string]$BorgUri, + + [Parameter(Mandatory = $true)] + [string]$ApiKey + ) + + #Remove from BoRG + $deletePlatformElementInventory = New-Object System.Collections.ArrayList + + foreach ($element in $ElementsToRemove) { + $delete = New-PlatformElementDetailPSObject ` + -EnvironmentTypeName $EnvironmentTypeName ` + -ElementName $element.Name + + $deletePlatformElementInventory.Add($delete) | Out-Null + } + + if ($deletePlatformElementInventory.Count -gt 0) { + Write-Host "Deleting [$($deletePlatformElementInventory.Count)] PlatformElementDetail items from BoRG" + (Remove-PlatformElementDetails -PlatformElementDetailArray $deletePlatformElementInventory.ToArray() -BorgUri $BorgUri -ApiKey $ApiKey) | Out-Null + } else { + Write-Host "No PlatformElementDetail items deleted in BoRG" + } + + #Add/Update BoRG + $addPlatformElementInventory = New-Object System.Collections.ArrayList + + foreach ($element in $ElementsToAdd) { + $detail = New-PlatformElementDetailPSObject ` + -EnvironmentTypeName $EnvironmentTypeName ` + -PlatformVersionName $PlatformVersionName ` + -ElementTierName $ElementTierName ` + -ElementName $element.Name ` + -ElementVersionName $element.Version + + $addPlatformElementInventory.Add($detail) | Out-Null + } + + if ($addPlatformElementInventory.Count -gt 0) { + Write-Host "Adding/Updating [$($addPlatformElementInventory.Count)] PlatformElementDetail items in BoRG" + (New-PlatformElementDetails -PlatformElementDetailArray $addPlatformElementInventory.ToArray() -BorgUri $BorgUri -ApiKey $ApiKey) | Out-Null + } else { + Write-Host "No PlatformElementDetail items added to BoRG" + } + + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Inventory/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Inventory/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Inventory/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.Inventory/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.nuspec b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.nuspec new file mode 100644 index 0000000..d0bdd5e --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.nuspec @@ -0,0 +1,25 @@ + + + + Alkami.DevOps.Minikube + $version$ + Alkami Platform Modules - DevOps - Minikube + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Minikube + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Minikube module for use with PowerShell. + + PowerShell + Copyright (c) 2022 Alkami Technologies + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.psd1 b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.psd1 new file mode 100644 index 0000000..c9b8b19 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.psd1 @@ -0,0 +1,11 @@ +@{ + RootModule = 'Alkami.DevOps.Minikube.psm1' + ModuleVersion = '1.0.6' + GUID = '65934fe9-ddda-47fe-809c-674429c85d74' + Author = 'twitecki' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2022 Alkami Technologies, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Export-MinikubeDevDynamic','Install-MinikubeDependencies','Reset-MiniKubeSecrets','Reset-MinikubeServices','Restart-Wsl','Start-Minikube' +} diff --git a/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.pssproj b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.pssproj new file mode 100644 index 0000000..ee6b757 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Alkami.DevOps.Minikube.pssproj @@ -0,0 +1,40 @@ + + + Debug + 2.0 + {d0e8804a-489a-4a1c-858e-c71d106880df} + Exe + Alkami.DevOps.Minikube + Alkami.DevOps.Minikube + Alkami.DevOps.Minikube + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Minikube") + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Minikube/Private/Get-MinikubeConfigurationInformation.ps1 b/Modules/Alkami.DevOps.Minikube/Private/Get-MinikubeConfigurationInformation.ps1 new file mode 100644 index 0000000..9d5466b --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Private/Get-MinikubeConfigurationInformation.ps1 @@ -0,0 +1,34 @@ +function Get-MinikubeConfigurationInformation { +<# + .SYNOPSIS + Gets the Minikube configuration information used to configure Alkami applications in Minikube + + .DESCRIPTION + Gets the Minikube configuration information used to configure Alkami applications in Minikube + + .EXAMPLE + Get-MinikubeConfigurationInformation +#> + [CmdletBinding()] + param() + + $ErrorActionPreference = "Stop" + + $localKubernetesConfigurationFolder = ".alkami-k8s-dev" + $localK8sConfigurationPath = "${HOME}/${localKubernetesConfigurationFolder}"; + $localServiceVersionsFileName = "values.local-service-versions.yaml" + $localServiceVersionsFilePath = "$localK8sConfigurationPath/$localServiceVersionsFileName" + $localServiceCustomizationsFileName = "values.local-service-configs.yaml" + $localServiceCustomizationsFilePath = "$localK8sConfigurationPath/$localServiceCustomizationsFileName" + $helmChartName = "alkami-local-dev" + + return @{ + localKubernetesConfigurationFolder=$localKubernetesConfigurationFolder + localK8sConfigurationPath=$localK8sConfigurationPath + localServiceVersionsFileName=$localServiceVersionsFileName + localServiceVersionsFilePath=$localServiceVersionsFilePath + localServiceCustomizationsFileName=$localServiceCustomizationsFileName + localServiceCustomizationsFilePath=$localServiceCustomizationsFilePath + helmChartName=$helmChartName + }; +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Export-MinikubeDevDynamic.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Export-MinikubeDevDynamic.ps1 new file mode 100644 index 0000000..a04ba63 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Export-MinikubeDevDynamic.ps1 @@ -0,0 +1,43 @@ +function Export-MinikubeDevDynamic { +<# + .SYNOPSIS + Exports the existing developer dynamic databases into their own container that is importable by someone else + + .DESCRIPTION + Exports the existing developer dynamic databases into their own container that is importable by someone else + + .EXAMPLE + Export-MinikubeDevDynamic +#> + [CmdletBinding()] + param() + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + $resourcesPath = Join-Path $PSScriptRoot "Resources" + + Write-Host "$logLead : Updating kubectl context to minikube" + minikube update-context + + Write-Host "$logLead : Exporting database in minikube" + $minikubeJson = docker inspect minikube + $minikubeContainerId = (ConvertFrom-Json -InputObject "$minikubeJson").Id + Write-Host "$logLead : Minikube container id: $minikubeContainerId" + docker cp ${minikubeContainerId}:/mnt/data "$PSScriptRoot" + + Write-Host "$logLead : Building docker image with exported DB" + $exportDockerFilePath = "$resourcesPath\DatabaseExportDockerfile" + $guid = [guid]::NewGuid().toString() + $tag = "export-$env:UserName-$guid" + $localDockerImageName = "alkami.db.developerdynamic.export:$tag" + docker build -t "$localDockerImageName" -f $exportDockerFilePath "$PSScriptRoot" + + Remove-Item -Recurse -Force "$PSScriptRoot\data" + + Write-Host "$logLead : Successfully built $localDockerImageName. Pushing to proget" + $progetImage = "packagerepo.orb.alkamitech.com/alkami/library/alkami.db.developerdynamic:$tag" + docker tag "alkami.db.developerdynamic.export:$tag" "$progetImage" + docker push "$progetImage" + + Write-Host "$logLead : Exported minikube database to: $progetImage" +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Install-MinikubeDependencies.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Install-MinikubeDependencies.ps1 new file mode 100644 index 0000000..af69686 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Install-MinikubeDependencies.ps1 @@ -0,0 +1,65 @@ +function Install-MinikubeDependencies { +<# + +.SYNOPSIS + Installs the dependencies required to run Alkami services in Minikube + +.DESCRIPTION + Installs the dependencies required to run Alkami services in Minikube + +.EXAMPLE + Install-MinikubeDependencies +#> + [CmdletBinding()] + param( + ) + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + + $chocoVersion = choco -v + + Write-Host "$loglead : Installing Minikube dependencies" + + if ($chocoVersion) { + Write-Host "$loglead : Using choco version: $chocoVersion" + } + else { + Write-Host "$loglead : Chocolatey not installed. Install Chocolatey before trying again." + } + + Write-Host "$loglead : Installing Minikube..." + choco install minikube --version 1.25.1 -y + + Write-Host "$loglead : Installing the kubectl..." + choco install kubernetes-cli --version 1.22.2 -y + + Write-Host "$loglead : Installing helm..." + choco install kubernetes-helm --version 3.7.2 -y + + Write-Host "$loglead : Installing skaffold..." + choco install skaffold --version 1.39.0 -y + + Write-Host "$loglead : Installing yaml tools for powershell..." + choco install powershell-yaml --version 0.4.2 -y + + $kerberosInstalled = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object { $_.DisplayName -Match 'MIT Kerberos for Windows' }) + If (-Not $kerberosInstalled) { + Write-Host "$loglead : Installing mitkerberos..." + choco install mitkerberos --version 4.1 -y + } + + $url = "alk-devdynamic.localhost" + Write-Host "$loglead : Checking if $url needs to be added to hosts..." + $hostsContent = Get-HostsFileContent + $hostsString = "127.0.0.1{0}{1}" + $hostsRegex = "^127\.0\.0\.1\s+$url" + + if (($hostsContent | Where-Object {$_ -match $hostsRegex }).Count -gt 0) { + Write-Host "$loglead : Loopback Hosts File Entry for $url already exists" + } else { + Write-Host "$loglead : Adding alk-devdynamic.localhost to hosts..." + $newHostsEntry = ($hostsString -f "`t`t", $url) + Add-HostsFileContent -contentToAdd $newHostsEntry -Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Reset-MiniKubeSecrets.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Reset-MiniKubeSecrets.ps1 new file mode 100644 index 0000000..d6b3523 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Reset-MiniKubeSecrets.ps1 @@ -0,0 +1,91 @@ +function Reset-MinikubeSecrets { +<# + .SYNOPSIS + Resets secrets used in the local service development environment + + .DESCRIPTION + Resets secrets used in the local service development environment: aws access key, ecr access key, kerberos ticket + + .PARAMETER AwsProfile + [string] Will use the specified AWS profile when refreshing AWS credentials + + .EXAMPLE + Refresh-Secrets -AwsProfile SRE +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$AwsProfile = "Dev" + ) + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + $tempAwsProfile = "temp-$AwsProfile".ToLower() + + Write-Host "$logLead : Verifying kube context..." + + kubectl config use-context minikube + + Write-Host "$logLead : Refreshing aws session with profile: $tempAwsProfile..." + + Update-AWSProfile -Profile $AwsProfile + $AWS_ECR_LOGIN = aws ecr get-login-password --region us-east-1 --profile $tempAwsProfile + docker login --username AWS --password $AWS_ECR_LOGIN 327695573722.dkr.ecr.us-east-1.amazonaws.com + docker login --username AWS --password $AWS_ECR_LOGIN 790953160341.dkr.ecr.us-east-1.amazonaws.com + + Write-Host "$logLead : Re-Mounting docker credentials for ECR access..." + + $DOCKER_AWS_AUTH = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("AWS:$AWS_ECR_LOGIN")) + $DOCKER_CONFIG_JSON = " + { + 'auths': { + '327695573722.dkr.ecr.us-east-1.amazonaws.com': { + 'auth': '$DOCKER_AWS_AUTH' + }, + '790953160341.dkr.ecr.us-east-1.amazonaws.com': { + 'auth': '$DOCKER_AWS_AUTH' + } + } + }" -replace "'", '"' + + $DOCKER_CONFIG_JSON | Out-File dockerconfig.json -Encoding Ascii + + if (kubectl get secret awsecr-cred -n localhost --ignore-not-found --output=yaml) { + Write-Host "$logLead : Deleting existing Kubernetes ECR access secret: awssecr-cred..." + kubectl delete secret awsecr-cred -n localhost + } + + Write-Host "$logLead : Creating Kubernetes ECR access secret: awssecr-cred..." + kubectl create secret generic awsecr-cred -n localhost --from-file=.dockerconfigjson=dockerconfig.json --type=kubernetes.io/dockerconfigjson + Remove-Item dockerconfig.json + + Write-Host "$logLead : Re-Mounting config map for AWS resource access..." + + $AWS_ACCESS_KEY_ID = aws configure get aws_access_key_id --profile $tempAwsProfile + $AWS_SECRET_ACCESS_KEY = aws configure get aws_secret_access_key --profile $tempAwsProfile + $AWS_SESSION_TOKEN = aws configure get aws_session_token --profile $tempAwsProfile + + if (kubectl get configmap aws-config -n localhost --ignore-not-found --output=yaml) { + Write-Host "$logLead : Deleting existing Kubernetes AWS access config map: aws-config..." + kubectl delete configmap aws-config -n localhost + } + + Write-Host "$logLead : Creating Kubernetes AWS access config map: aws-config..." + kubectl create configmap aws-config -n localhost --from-literal=AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID --from-literal=AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY --from-literal=AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN + + Write-Host "$logLead : Re-Mounting Kerberos ticket for database access..." + + Write-Host "$logLead : Purging existing Kerberos tickets..." + klist purge + + Write-Host "$logLead : Obtaining new Kerberos ticket..." + kinit + + if (kubectl get configmap kerberos-config -n localhost --ignore-not-found --output=yaml) { + Write-Host "$logLead : Deleting existing Kerberos ticket config map: kerberos-config..." + kubectl delete configmap kerberos-config -n localhost + } + + Write-Host "$logLead : Creating Kerberos ticket config map: kerberos-config..." + kubectl create configmap kerberos-config -n localhost --from-file=c:\ProgramData\MIT\Kerberos5\ +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Reset-MinikubeServices.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Reset-MinikubeServices.ps1 new file mode 100644 index 0000000..704b40c --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Reset-MinikubeServices.ps1 @@ -0,0 +1,47 @@ +function Reset-MinikubeServices { +<# + .SYNOPSIS + Resets Minikube services to match those defined in the service version and configuration files + + .DESCRIPTION + Use this function to reset your cluster to match your service(s) configuration files. + + .PARAMETER Force + [switch] Will force a complete reinstallation and sync up between the services config file and the Minikube cluster. Use this when removing or excluding a service from the configuration. + + .EXAMPLE + Refresh-Services -Force +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + + $config = Get-MinikubeConfigurationInformation + + if ($Force.IsPresent) + { + Write-Host "$loglead : Forcing refresh of services..." + + Write-Host "$loglead : Deleting localhost namespace..." + kubectl delete ns localhost + + Write-Host "$loglead : Creating localhost namespace..." + kubectl create ns localhost + + Write-Host "$loglead : Refreshing Minikube secrets..." + Reset-MinikubeSecrets + + Write-Host "$loglead : Installing alkami-local-dev services..." + helm install alkami-local-dev --repo https://packagerepo.orb.alkamitech.com/helm/helm-charts $config.helmChartName -n localhost --create-namespace --dependency-update -f $config.localServiceVersionsFilePath -f $config.localServiceCustomizationsFilePath + } + else + { + Write-Host "$loglead : Upgrading alkami-local-dev services..." + helm upgrade alkami-local-dev --repo https://packagerepo.orb.alkamitech.com/helm/helm-charts $config.helmChartName -n localhost --create-namespace --dependency-update -f $config.localServiceVersionsFilePath -f $config.localServiceCustomizationsFilePath + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Restart-Wsl.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Restart-Wsl.ps1 new file mode 100644 index 0000000..8ceffb8 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Restart-Wsl.ps1 @@ -0,0 +1,46 @@ +function Restart-Wsl { +<# + .SYNOPSIS + Restarts Wsl and waits for it to restart + + .DESCRIPTION + Restarts Wsl and waits for it to restart + + .EXAMPLE + Restart-Wsl +#> + [CmdletBinding()] + param() + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + + Write-Host "$logLead : Stopping all Docker Desktop processes" + Get-Process "*docker desktop*" | Stop-Process + + Write-Host "$logLead : Shutting down WSL" + + wsl --shutdown + + Write-Host "$logLead : Waiting for Wsl to shutdown" + Start-Sleep -Seconds 5 + + Write-Host "$logLead : Starting all Docker Desktop processes" + Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe" + + Write-Host "$logLead : Waiting for Docker Desktop to startup" + + $job = Start-Job -ScriptBlock { + do + { + docker version + } + while($lastexitcode -ne 0) + } + + Wait-Job -Job $job | Out-Null + + Remove-Job -Job $job + + Write-Host "$logLead : Wsl successfully restarted" +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Public/Start-Minikube.ps1 b/Modules/Alkami.DevOps.Minikube/Public/Start-Minikube.ps1 new file mode 100644 index 0000000..525198e --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Public/Start-Minikube.ps1 @@ -0,0 +1,206 @@ +function Start-Minikube { +<# + .SYNOPSIS + Starts a Minikube Kubernetes cluster ready to host Alkami applications + + .DESCRIPTION + Starts a Minikube Kubernetes cluster ready to host Alkami applications + + .PARAMETER InstallDependencies + [switch] Will install all dependencies for running Minikube locally. Run this the first time starting Minikube. + + .PARAMETER IncludeMetricsServer + [switch] Will include metrics server as part of the Minikube cluster. + + .PARAMETER IncludeKibana + [switch] Will include Kibana as part of the Minikube cluster. + + .PARAMETER IncludeArgoCD + [switch] Will include ArgoCD and Argo Rollouts as part of the Minikube cluster + + .PARAMETER ForceReset + [switch] Will delete and recreate the entire minikube cluster. Required when wanting to edit the CPU and Memory allocation. + + .PARAMETER Cpus + [uint32] Will set the Minikube CPU resource allocation to the specificed number of CPUs. + + .PARAMETER Memory + [uint32] Will set the Minikube memory resource allocation to the specificed number of MegaBytes. + + .PARAMETER IngressPort + [uint32] Will expose ingress traffic into the Minikube cluster on the specified port. Default: 10000 + + .EXAMPLE + Start-Minikube -InstallDependencies -IncludeMetricsServer -Cpus 6 -Memory 8 -IngressPort 7000 +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [switch]$InstallDependencies, + + [Parameter(Mandatory = $false)] + [switch]$IncludeMetricsServer, + + [Parameter(Mandatory = $false)] + [switch]$IncludeKibana, + + [Parameter(Mandatory = $false)] + [switch]$IncludeArgoCD, + + [Parameter(Mandatory = $false)] + [switch]$ForceReset, + + [Parameter(Mandatory = $false)] + [uint32]$Cpus = 5, + + [Parameter(Mandatory = $false)] + [uint32]$Memory = 6, + + [Parameter(Mandatory = $false)] + [uint32]$IngressPort = 10000 + ) + + $ErrorActionPreference = "Stop" + $logLead = (Get-LogLeadName) + $resourcesPath = Join-Path $PSScriptRoot "Resources" + $config = Get-MinikubeConfigurationInformation + + if ($InstallDependencies.IsPresent) { + Install-MinikubeDependencies + } + + if ($ForceReset.IsPresent) { + minikube delete + } + + # Configure WSL memory usage. + $wslConfigPath = "${Env:HOMEPATH}\.wslconfig" + Write-Host "$logLead : Configuring WSL resource settings at: $wslConfigPath." + $memoryLimit = "$($Memory)GB" + $wslConfig = @" +[wsl2] +memory=$memoryLimit +swap=0 +"@ + + Set-Content -Path $wslConfigPath -Value $wslConfig -Force + + Restart-Wsl + + [string]$certsDirectory= "$resourcesPath\cacerts\*" + + [string]$miniKubeCertsDirectory = "${Env:HOMEPATH}\.minikube\certs" + + if (!(Test-Path $miniKubeCertsDirectory)) + { + New-Item $miniKubeCertsDirectory -ItemType Directory + } + + Write-Host "$logLead : Copying CA Certs to minikube cert location: ${miniKubeCertsDirectory}" + + Copy-item -Force -Recurse $certsDirectory -Destination $miniKubeCertsDirectory + + # The wslconfig allocates memory for wsl. Docker desktop doesn't get all of that memory so minikube will need less than 1024 bytes per gig allocated to wsl. + $memoryBytes = $Memory * 800 + + minikube config set cpus $Cpus + minikube config set memory $memoryBytes + + minikube start --driver=docker ` + --embed-certs ` + --addons dashboard ` + --addons ingress ` + --docker-opt=dns=10.0.16.42 ` + --docker-opt=dns=10.0.16.43 ` + --docker-opt=dns-search=corp.alkamitech.com ` + --docker-opt=dns-search=fh.local ` + --ports=$($IngressPort):443 ` + --ports=32000:32000 ` + --extra-config=kubelet.housekeeping-interval=10s + + if ($IncludeMetricsServer.IsPresent) { + minikube addons enable metrics-server + } + + Write-Host "$logLead : Copying Kerberos configuration file. (krb5.ini)" + Copy-Item (Join-Path $resourcesPath "\krb5.ini") -Destination "C:\ProgramData\MIT\Kerberos5" -Force + + Write-Host "$logLead : Configuring Kerberos ticket cache location environment variable." + # Set env variable for current session + $Env:KRB5CCNAME = "c:\ProgramData\MIT\Kerberos5\krb5cc_0" + # Set env variable for future sessions + [System.Environment]::SetEnvironmentVariable('KRB5CCNAME','c:\ProgramData\MIT\Kerberos5\krb5cc_0', [System.EnvironmentVariableTarget]::Machine) + + Write-Host "$logLead : Creating localhost namespace..." + + kubectl create ns localhost + + Write-Host "$logLead : Initializing Kubernetes secrets..." + + Reset-MinikubeSecrets + + Write-Host "$logLead : Configuring tls for ingress" + kubectl apply -f (Join-Path $resourcesPath "ingress-tls-secret.yaml") + + $ingressDeploy = kubectl get deployment/ingress-nginx-controller -n ingress-nginx -o yaml | ConvertFrom-Yaml + + Write-Host "$logLead : Updating Ingress Nginx with proxy forwarding enabled..." + $ingressDeets = kubectl get cm ingress-nginx-controller -n ingress-nginx -o yaml | ConvertFrom-Yaml + + $ingressDeets.Data.Add('use-forwarded-headers', 'true') + $ingressDeets | ConvertTo-Yaml | kubectl apply -f - + + Write-Host "$logLead : Deleting ingress nginx deployment" + kubectl delete deployment/ingress-nginx-controller -n ingress-nginx + + $ingressDeploy.spec.template.spec.containers[0].args += "--default-ssl-certificate=ingress-nginx/ingress-tls-secret" + $ingressDeploy.metadata.Remove('managedFields') + $ingressDeploy.Remove('status') + $modifiedDeploy = $ingressDeploy | ConvertTo-Yaml + + Write-Host "$logLead : Recreating ingress nginx deployment with default ssl certificiate" + $modifiedDeploy | kubectl apply -f - + + Write-Host "$logLead : Waiting for ingress nginx controller to be healthy before proceeding..." + kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx + + kubectl apply -f (Join-Path $resourcesPath "ingress-dashboard.yaml") + + Write-Host "$logLead : Checking for local kubernetes configuration file path: ${$localK8sConfigurationPath}" + + if (-not (Test-Path $config.localK8sConfigurationPath)) { + Write-Host "$logLead : Missing local kubernetes configuration file path. Creating..." + New-Item -path $HOME -name $config.localKubernetesConfigurationFolder -type "directory" + New-Item -path $config.localK8sConfigurationPath -name $config.localServiceVersionsFileName -type "file" -value "# Use this file to configure which services to install. You can copy any existing gitops values file for any environment - for example: https://bitbucket.corp.alkami.net/projects/AUTO/repos/alkami.gitops.kubernetes/browse/alkami-services/environments/tde/values.tde.yaml" + New-Item -path $config.localK8sConfigurationPath -name $config.localServiceCustomizationsFileName -type "file" -value "# Use this file to customize any service definitions via env variables etc. You can also exclude a service from being installed into your local environment (exclude: true)" + } else { + Write-Host "$logLead : Found existing local kubernetes configuration file path at: $localK8sConfigurationPath" + } + + if (!(helm repo list | select-string proget)) { + Write-Host "$logLead : Missing helm repo configuration. Adding proget repo..." + helm repo add proget "https://packagerepo.orb.alkamitech.com/helm/helm-charts" + } + + if ($IncludeArgoCD.IsPresent) { + Write-Host "$logLead : Adding ArgoCD helm repo configuration..." + helm repo add argo https://argoproj.github.io/argo-helm + helm repo add argo-rollouts https://argoproj.github.io/argo-helm + } + + Write-Host "$logLead : Updating helm repos..." + helm repo update + + Write-Host "$logLead : Installing alkami-local-dev helm chart..." + helm install alkami-local-dev --repo https://packagerepo.orb.alkamitech.com/helm/helm-charts $config.helmChartName -n localhost --create-namespace --dependency-update -f $config.localServiceVersionsFilePath -f $config.localServiceCustomizationsFilePath + + if ($IncludeKibana.IsPresent) { + helm install alkami-dev-kibana --repo https://packagerepo.orb.alkamitech.com/helm/helm-charts alkami-dev-kibana --dependency-update + } + + if ($IncludeArgoCD.IsPresent) { + $currentDate = (get-date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") + helm install argocd argo/argo-cd -n argocd --create-namespace --set server.ingress.enabled=true --set server.ingress.hosts[0]=argocd.localhost.dev.alkami.net --set server.extraArgs[0]='--insecure' --set config.secret.extraArgs[1]='--disable-auth' --set configs.secret.argocdServerAdminPassword='$2y$10$CyYdVLTiR8OO2gGwkQsAeuwAFYeSOzPH6Kf/aan7fLau57fgVaUaq' --set configs.secret.argocdServerAdminPasswordMtime="$currentDate" + helm install argo-rollouts argo-rollouts/argo-rollouts -n argo-rollouts --create-namespace + } +} diff --git a/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamica.pem b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamica.pem new file mode 100644 index 0000000..b73c925 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamica.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE9zCCA9+gAwIBAgIJAJJDI3J7eaSmMA0GCSqGSIb3DQEBBQUAMIGtMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJTG9zIEFsdG9z +MRUwEwYDVQQKEwxuZXRTa29wZSBJbmMxGDAWBgNVBAsTD0NlcnQgTWFuYWdlbWVu +dDEdMBsGA1UEAxMUY2FhZG1pbi5uZXRza29wZS5jb20xJTAjBgkqhkiG9w0BCQEW +FmNlcnRhZG1pbkBuZXRza29wZS5jb20wHhcNMTMwNjE5MjMyMTE3WhcNNDMwNjEy +MjMyMTE3WjCBrTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQ +BgNVBAcTCUxvcyBBbHRvczEVMBMGA1UEChMMbmV0U2tvcGUgSW5jMRgwFgYDVQQL +Ew9DZXJ0IE1hbmFnZW1lbnQxHTAbBgNVBAMTFGNhYWRtaW4ubmV0c2tvcGUuY29t +MSUwIwYJKoZIhvcNAQkBFhZjZXJ0YWRtaW5AbmV0c2tvcGUuY29tMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzKWllimAaCY6P8qG1OjPrs4/5b/ofQX +e7Gd/vCoAtWEClQ6TxC1YkUl7sYF9bu1CUD3NHWsQfReezCKGyBPafcu+twg+Dcd +yc/uaCxz6V9BpsPT6lgzYkDDpVE63I9zlMGnaS+hIAdplaPjcXHUXe/mYNXtKAte +duaUDg76R1588BLvOVWn6txFoDxKqDsfkzAoI3uoPO7AH/0EFAokAYzt9f1G1hLE +v30U9feQ0OIgGSpdiqo/pLDaRUpAI2ZmOfm5DNu/6vEF0G6p7rNY1rIUynTVmzKY +P+5w61fsBN19OUYCvCgF6NOpBZgl93xCocnZ1Zd3DjR9M2QFFdXrXwIDAQABo4IB +FjCCARIwHQYDVR0OBBYEFNQ+GYzg+VGt/zUO4p1Ja1DuzKF+MIHiBgNVHSMEgdow +gdeAFNQ+GYzg+VGt/zUO4p1Ja1DuzKF+oYGzpIGwMIGtMQswCQYDVQQGEwJVUzET +MBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJTG9zIEFsdG9zMRUwEwYDVQQK +EwxuZXRTa29wZSBJbmMxGDAWBgNVBAsTD0NlcnQgTWFuYWdlbWVudDEdMBsGA1UE +AxMUY2FhZG1pbi5uZXRza29wZS5jb20xJTAjBgkqhkiG9w0BCQEWFmNlcnRhZG1p +bkBuZXRza29wZS5jb22CCQCSQyNye3mkpjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBBQUAA4IBAQCKwspwM/SpIrbFEKSh4bfhR6p9YZ8nL6V5Q68lQooIFBTndPi4 +0lazoss4NrUchuCvDe9LHgfDQMf5LABiyy6RixMhWYa85hoUHkULDwunSYHJEKhH +OzuxQl31m7/jQtL7RtAvTzdnykI5lrGdOjvCgxFDa6lBS5fmDUs+5DpDbHWamExC +ksBmuDhV8+yh+7MZriSOCEzDNlxUm8qbK2TYvaQhM7n+YM4BG+2DVWtVPtyiVTFd +ss27AwG2hyhc+Czg5PU2V1Z+JsQyEOl4G8xLHH6hdQPWC8gBlsPc/nemEBxhnqNi +fAkUAvHXsEwI5PdVcvP2KI4hSz60FQNMQEr6 +-----END CERTIFICATE----- diff --git a/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamienterpriseca.pem b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamienterpriseca.pem new file mode 100644 index 0000000..9444d5a --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/alkamienterpriseca.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIQIcBw3ESSAKxPclLhwj+AwjANBgkqhkiG9w0BAQsFADB1 +MRMwEQYKCZImiZPyLGQBGRYDY29tMRowGAYKCZImiZPyLGQBGRYKYWxrYW1pdGVj +aDEUMBIGCgmSJomT8ixkARkWBGNvcnAxLDAqBgNVBAMTI0Fsa2FtaSBFbnRlcnBy +aXNlIFRydXN0ZWQgUm9vdCBDQTAxMB4XDTIwMDcyOTE2MzE1NFoXDTMyMDcyOTE2 +NDE1MFowdTETMBEGCgmSJomT8ixkARkWA2NvbTEaMBgGCgmSJomT8ixkARkWCmFs +a2FtaXRlY2gxFDASBgoJkiaJk/IsZAEZFgRjb3JwMSwwKgYDVQQDEyNBbGthbWkg +RW50ZXJwcmlzZSBUcnVzdGVkIFJvb3QgQ0EwMTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBALf7suNprUZJLTwZSYLKa4ZtZXb3mijr5kaCliBQ2s+zCFiK +IQ7VeV2PGU2HhQ7TI083mdHpkReiJC/XnZG5OLgNV34PszL4vaB/pfs/D11/Yl0J +q/I03YAjLuvN1B0ncu34jI+Lt0Skn/qS4+LN5DcB9UEx6LB+tO62cLkduh1fAxid +NHE3Y6Vv8HaNS31wxzz9v8cwdc6HE28Mo1s7yH+0KxhHymZZEE16zD/a3SBpr0uF +f/jiZt7JfvB7mmZW3RD7EUkyjt5Zey/XSzzUPyqaSP/9nxDDdSAN1MXJUCJmJNE/ +hg162DxBnbw9dK7wJ0GnC2CZCpp5fOdjmomJgr2KOAc/LtIqlfbeafEGN7CKN9tw +RI8Ifh0C1A+TUmQ5pm9AoUsV++E0xS/oQw2BqxInnLXwC6HbdepQr2t3GmVrHCh7 +RdGIZBuMyFW6GO8VooSjvewhZnaEUCBQccfe1u4qLb65FKH4LCedPcZJj2I2Q+Rq +gtqz9ixrdLcSfEUlR06E0LjSECYlwv3b1saW0U3m9MYh7rv0pC+h91FEh+mR+if4 +jxFoippDzeuAjdpB9CZWmJLrh1FlLk62LWUKWpIyFXMFnvQGEb3V09NJT/R3zoL6 +r10q7u4UDm+MXwyZHSDmOwgIpgHD0O5TR4goKJ5OE+B+5MqupVGYtU+9NuCXAgMB +AAGjaTBnMBMGCSsGAQQBgjcUAgQGHgQAQwBBMA4GA1UdDwEB/wQEAwIBhjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ3LgGw9Kf7bhIGQusLIAR4GSpxYDAQBgkr +BgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAZn6erjl5ySbFSuPq9AYR +gcI4BNGUf19GS8ahAzzCqH9W8a3s1AOC4PjCYUH1DZWco/WY8DlhsKqOL7lyiMoL +hGxVdD4PYKgSOqmNBinNEl0xRQ1FKdjxygxIFU7X970ZiZsTJKV/gDqS90HfY3uL +YGZMWDChIYONtZnmxMr3D/rWn8K0a+ziDbrDgnLVAy6X7pw4uefRBfm5TqVi66Ak +F2Tj4IeBf5QFUGLXVQNzac/Bn6Wh0y0d9TcHfZT4uwvREB5v6RWmTcAqUqLCyDzu +i5hq+mzegW7KZz9gLnfBdrVEG9vR4ZiDwZy3J02PHTT/zfmtAd65sw0QJwfiLWgg +Mpc1+StwBlLaX6QZMY+PEifc108TV8TREUew4dBRJdZY3Yx4Pk9qbWqP0dN/DAs+ +uQcET1pxVqozad8zBcXEd7g80bmOKF6XYgOUeqVjR3Gha6phLz679A4NcpbqTzl6 +i+FO9quLlNgzcqwqqANSsdC2pGCYFhYMOgRU5ygQr/89Bun4cvLuOctToeZg+0by +janN/o4r+xRIvUTRdrF88v5gpLmLHE4Giq5JLmpPAXxjHPnwhh5+Mrozp1XLvDDt +Jchu44wugkOJuCJnBQUu1HXi99GhEk4Wr3UZ/QRUaor7KcxiTnGfvX5h1HnPTNps +Kmw7n+AmxIMk8zaCHky7HsQ= +-----END CERTIFICATE----- diff --git a/Modules/Alkami.DevOps.Minikube/Resources/cacerts/bitbucket.pem b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/bitbucket.pem new file mode 100644 index 0000000..69592f5 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/bitbucket.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF6DCCBNCgAwIBAgIQBHWZSb1ru3eGnAuhZXHHITANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg +Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMTA3MTQwMDAwMDBaFw0yMjA4MTIy +MzU5NTlaMCQxIjAgBgNVBAMTGWJpdGJ1Y2tldC5jb3JwLmFsa2FtaS5uZXQwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuhOmLtN6LXrJbRO/a4aXViJjM +kJTTxZ4s4iE2As43XKgqtgzGIBBwox+rLH/LPzuoBN5p7D7QlhHb8P8KKFvX62/P +ixIIABfsZ/jJOBh4PB0pLTMucysU+0sMoAIn7sAqnGHIiq385UMiWt6tv9rcx1vi +7VB5wg0sQjJ/sPWHJLFw8kZM9ooERRTBtGfP5NbUJk9ex6YDadbH0c/Murp/9yCQ +G2/8yA8U9UOawDn7yBeAvPTNID3uhgqW3IPEl97huNdbnFOBXZ9RhDqtfsaQc3YI +5CJEIvyyIQxLmnQVGIRk0BkntgBXJ4s0Gh9laN/GPO/aIzN1c9SQSaNzhmhjAgMB +AAGjggLyMIIC7jAfBgNVHSMEGDAWgBRZpGYGUqB7lZI8o5QHJ5Z0W/k90DAdBgNV +HQ4EFgQUNoEYTY74OzP+E7SMgtWF1rrQER8wJAYDVR0RBB0wG4IZYml0YnVja2V0 +LmNvcnAuYWxrYW1pLm5ldDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMDsGA1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmwuc2Nh +MWIuYW1hem9udHJ1c3QuY29tL3NjYTFiLmNybDATBgNVHSAEDDAKMAgGBmeBDAEC +ATB1BggrBgEFBQcBAQRpMGcwLQYIKwYBBQUHMAGGIWh0dHA6Ly9vY3NwLnNjYTFi +LmFtYXpvbnRydXN0LmNvbTA2BggrBgEFBQcwAoYqaHR0cDovL2NydC5zY2ExYi5h +bWF6b250cnVzdC5jb20vc2NhMWIuY3J0MAwGA1UdEwEB/wQCMAAwggF+BgorBgEE +AdZ5AgQCBIIBbgSCAWoBaAB1ACl5vvCeOTkh8FZzn2Old+W+V32cYAr4+U1dJlwl +XceEAAABeqbAEhQAAAQDAEYwRAIgNBQSZ5T+VkjP2IXiHcg3DEanOYFKkSi28uvk +nS8KyVsCICnDtAu11R68/culSg1ZwMGbB7x5+LhpdvDkAn9xN4OiAHYAQcjKsd8i +RkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvYAAAF6psASHQAABAMARzBFAiEA0JRT +UThxXp2dcsKowmthUSbyCL33lUx0GUw7BKRVL+sCICP+A5Ehj9Lvo1imko46MUJS +zWvfzBeuvP4bfPt1rW4pAHcA36Veq2iCTx9sre64X04+WurNohKkal6OOxLAIERc +KnMAAAF6psASdAAABAMASDBGAiEAjTB5aEFFq1KhvNHPzHfG4dYCwoXZfhiHt/Wr +wMek4k4CIQCgM6RefXs+V406RI2m/xDSREAmIo3kSVgBmJHUxprsGDANBgkqhkiG +9w0BAQsFAAOCAQEApzPcmbB/rPpX5dwSo8HxzmFMCkDKOm3Yow+KD4Eqtmpi+yup +agSTgp7/yGUn0zd/mDtCIqoFc3U+bVajzdzFYlcNYKKNNf9P1cWHBngq+IdswYO0 +9WhlHtn3dZNZLCciEC0qOC94LkKMyzazuMTJqunxInUU4be7rFfuoUITcXkl3nx+ +ayYlDbJEEj3pCd9VD7SSHvn/OIY6TfjcUWu6l7uG/JIygFJ6E4zbIz83XrgbA7cf +RjQ3CMghMt/6B+0U63shD3Ji+qwwgSFsjnvHDouEHp6dYwLVjQUzjMDYwVg8CkA7 +eXFwBpSYaiylvyNimorTtXVI+nsIpcuc9I7OUQ== +-----END CERTIFICATE----- diff --git a/Modules/Alkami.DevOps.Minikube/Resources/cacerts/crl5d.pem b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/crl5d.pem new file mode 100644 index 0000000..202ab17 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/cacerts/crl5d.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEhTCCA22gAwIBAgIRAI89BCr/SzvVCgAAAAErhhswDQYJKoZIhvcNAQELBQAw +RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM +TEMxEzARBgNVBAMTCkdUUyBDQSAxQzMwHhcNMjExMjI3MDkzMTQzWhcNMjIwMzIx +MDkzMTQyWjATMREwDwYDVQQDDAgqLmdjci5pbzBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABCFO2fjluCduNMWBZOF+f8oA2oo7aXMDBInGXLzp7u4rR9FsFwd52GDm +PQ0k75e9JxqC8TBYr0QljJ4DVMnlnuujggJqMIICZjAOBgNVHQ8BAf8EBAMCB4Aw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUckS8 +gIPpksW7z9gFprTdhDJ+knUwHwYDVR0jBBgwFoAUinR/r4XN7pXNPZzQ4kYU83E1 +HScwagYIKwYBBQUHAQEEXjBcMCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5wa2ku +Z29vZy9ndHMxYzMwMQYIKwYBBQUHMAKGJWh0dHA6Ly9wa2kuZ29vZy9yZXBvL2Nl +cnRzL2d0czFjMy5kZXIwGwYDVR0RBBQwEoIIKi5nY3IuaW+CBmdjci5pbzAhBgNV +HSAEGjAYMAgGBmeBDAECATAMBgorBgEEAdZ5AgUDMDwGA1UdHwQ1MDMwMaAvoC2G +K2h0dHA6Ly9jcmxzLnBraS5nb29nL2d0czFjMy9RT3ZKME4xc1QyQS5jcmwwggEF +BgorBgEEAdZ5AgQCBIH2BIHzAPEAdgApeb7wnjk5IfBWc59jpXflvld9nGAK+PlN +XSZcJV3HhAAAAX37cjC7AAAEAwBHMEUCIQCOKm34cS8aPgCbGjzzRMQPrwpJQYQw +CEG8TyLKKY1/aQIgGIBLbr6TjLzlr+/uERVzUebwM6IEY03X+LFpu8CeuukAdwBB +yMqx3yJGShDGoToJQodeTjGLGwPr60vHaPCQYpYG9gAAAX37cjDmAAAEAwBIMEYC +IQCaGySDyumyE8mY4fbbJBQTmTE7jaLWhPVB3VyvuxXF+wIhAK3fOwV5Bv35Bjwb +NvFyZzn62oRELEzUKL8pSO1+fR+IMA0GCSqGSIb3DQEBCwUAA4IBAQAZorur4aYb +UITVxa2TfK7IxKVSdHLOelv+gMjceaRuyCC7dWm/bk2RDE5ngPxT5ipsA86QYiYc +lIakDnNEsrMg8FDgp0gySyAznE4Ans3YGNuacxgZBrg9t7vVOmYb9+p6nG/BcNEC +4+4dgv4f8ILyTSG59zsIL6SIW3J1pAtJOSwffutS6FN/UzVVF8d7NG83Q6++tUx/ +15yqNFm5uR1pcUAP/jdElRjAgeDVUpG/yUTbRQHjOTNdRNzS7TGf8QmwoT23LnRI +icbPkO7yAsYbNBURah3t/MHFeWh1b8hNisZjbzeNMiBOi2DdQbP27mZ/6kSNJR3y +r1UPRxHWONlp +-----END CERTIFICATE----- diff --git a/Modules/Alkami.DevOps.Minikube/Resources/ingress-dashboard.yaml b/Modules/Alkami.DevOps.Minikube/Resources/ingress-dashboard.yaml new file mode 100644 index 0000000..f297749 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/ingress-dashboard.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kubernetes-dashboard-ingress + namespace: kubernetes-dashboard + annotations: + "helm.sh/resource-policy": keep +spec: + ingressClassName: nginx + tls: + - hosts: + - dashboard.localhost.dev.alkami.net + rules: + - host: dashboard.localhost.dev.alkami.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kubernetes-dashboard + port: + number: 9090 \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Resources/ingress-tls-secret.yaml b/Modules/Alkami.DevOps.Minikube/Resources/ingress-tls-secret.yaml new file mode 100644 index 0000000..a302eb9 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/ingress-tls-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ingress-tls-secret + namespace: ingress-nginx + annotations: + "helm.sh/resource-policy": keep +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVKRENDQXd5Z0F3SUJBZ0lSQUtzczAxNm5KT1hOSGVBc2lzZDM5YTR3RFFZSktvWklodmNOQVFFTEJRQXcKZGpFTE1Ba0dBMVVFQmhNQ1ZWTXhIakFjQmdOVkJBb01GVUZzYTJGdGFTQlVaV05vYm05c2IyZDVJRWx1WXpFTApNQWtHQTFVRUN3d0NTVlF4RGpBTUJnTlZCQWdNQlZSbGVHRnpNUm93R0FZRFZRUUREQkZ6ZFdKallURXVZV3hyCllXMXBMbTVsZERFT01Bd0dBMVVFQnd3RlVHeGhibTh3SGhjTk1qSXdNekE0TVRZd01qQXhXaGNOTWpNd05EQTQKTVRjd01qQXhXakFsTVNNd0lRWURWUVFEREJvcUxteHZZMkZzYUc5emRDNWtaWFl1WVd4cllXMXBMbTVsZERDQwpBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUlmV2lwWDA5MFc1TnNFZlZYOUJQYUl3Cm9LeVBEUzFpWU9Xb1NzemdWN3NhNDU0OXFhUDVUeFM0MFRmUmpaSXB2UTZhMnI1S3RMU0RlSEhnZEtTT2N5d2IKUllYWkcrS20rRytvRFlhbUFTUHJMamJqVVJtTXVxbnV2dUJRQjVOdlZzSlJEL0tzeUxodUtqN1EwTFhIT0FQWgpkQ1lpVkxCNi9jMEdxK0JTUWorSm9UR0NiQ215dXBDLzV5Q21wRlVpYjlVZXdNc29jSzlEMU96b1luTHNxY1czCkF5bGJEYnl3bTFuWEpRNUVOeHI5cE90bGpnM0V1M3o0bFNtdFlSbmFIQ1RHemtyQUJOZnByUit6TEwzZXRycysKQklhMHJQMC9CY0RyeG1BOFh1RzlCem1OSTRpTWNtN3d2WGR1TXMydnhtYkw2dlczaVljeGFHWUxMUDZQMkFjQwpBd0VBQWFPQi9UQ0IrakFsQmdOVkhSRUVIakFjZ2hvcUxteHZZMkZzYUc5emRDNWtaWFl1WVd4cllXMXBMbTVsCmREQUpCZ05WSFJNRUFqQUFNQjhHQTFVZEl3UVlNQmFBRkV3eHlqYThRZjJreU1NaEdISjIwTkwza2E0Zk1CMEcKQTFVZERnUVdCQlJ0aHZCMUxiVzhiU2tmNVVYMlNwYTFxak1jbmpBT0JnTlZIUThCQWY4RUJBTUNCYUF3SFFZRApWUjBsQkJZd0ZBWUlLd1lCQlFVSEF3RUdDQ3NHQVFVRkJ3TUNNRmNHQTFVZEh3UlFNRTR3VEtCS29FaUdSbWgwCmRIQTZMeTlqY213dWMyVmpMbUZzYTJGdGFTNXVaWFF2WTNKc0x6WTRORFkzT1RaaUxUZGpOV1V0TkdWaFpTMWkKTldObExUZ3hOVFJsWWpFek5UbGxZeTVqY213d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDNm1VbEg2ZmVVTApXOU45NCszNjBzaHByUkNudVZHaU83WkxxRG1PeXlReHJWMHJwRGFaQ2svamY5b3pQUDlqQ1U1UmhxQlhBNXNvCnNNbDhNLzJXcjQxeWUva29idHVZUGlNVUo1KzFPRXhEMWdDNVRzYlpmWDdnMWZ2Q0NYUEZ3UTV1Z29mT0tsQUIKNzVPTTQ5eFFhMkhaV1FTZFcvV2tjeUJMQVhmemlyeUgyYlg0TXJTeEhGdmZGbmZqZTJ4c2hRNWZwSFlLbHVlSgplK09FUWVxMXhMUzczUHpobEhFcU43aE9QSWVRc0pyanBvQk5YbkhKU0tOVG5IdGhuUExSemVEbkhVWTc1dG5XCnhSME1WaHlYYUtVbkZreTdnYTA3b1V1THZ0WGJNangyU2ZpZVdRdkREdGdRbTBwUFM4M0VreFg1eFNNLzUvL0kKVW5uQ01RTGNzNE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ0gxb3FWOVBkRnVUYkIKSDFWL1FUMmlNS0NzancwdFltRGxxRXJNNEZlN0d1T2VQYW1qK1U4VXVORTMwWTJTS2IwT210cStTclMwZzNoeAo0SFNram5Nc0cwV0YyUnZpcHZodnFBMkdwZ0VqNnk0MjQxRVpqTHFwN3I3Z1VBZVRiMWJDVVEveXJNaTRiaW8rCjBOQzF4emdEMlhRbUlsU3dldjNOQnF2Z1VrSS9pYUV4Z213cHNycVF2K2NncHFSVkltL1ZIc0RMS0hDdlE5VHMKNkdKeTdLbkZ0d01wV3cyOHNKdFoxeVVPUkRjYS9hVHJaWTROeEx0OCtKVXByV0VaMmh3a3hzNUt3QVRYNmEwZgpzeXk5M3JhN1BnU0d0S3o5UHdYQTY4WmdQRjdodlFjNWpTT0lqSEp1OEwxM2JqTE5yOFpteStyMXQ0bUhNV2htCkN5eitqOWdIQWdNQkFBRUNnZ0VBSGpWZGU0elBRcysyT0F1T1dXNzZWR3ZwQjRjR29LMnNxOVlaMEdjaHk0Zm0KejhXWnlOQUVRTEQ3UWlVVmpVZy85WlFGaW1VVnU4RXpFMndkdEl6RFd5OHpibGxDaE15cUdqYXV6MTl5aUhqZwpQYStlMVFaQmF0SWYyOFdnY2E3RWhoRTk4VE53cmVjOStOczZWdnFYWCsvSGowVjZQUWNWRXpmbEdFMWkwenNtCnp1UEhQVnlkVnNMMzJiT04wdWJISFpJNXFNaVQzenJMeE8rRWllQ0ZBQVkrSjVReDU1bUZlbnpybW1NVUdhU20KTUFNdVVLSnY2dndKS3Jtdk5RbStlVTZTdTBHT1lVbXVvRjc5N0FJY0FEQjdYWm9Ob05iTjYwUStDdUhyUU5oegpISW92bGpTTlpRck1ZajM2YVNmSUgxWHd2bEFNcXF4dnJySW9HTnc0SVFLQmdRQzdmeit1cFdRMjRXQSt4MDZUCm1SZko3U2xHOG9OWXZ5V0dHL3N6ZnVDYnlLOUttWHNyZTB1OXFpejBVYkY4Wm9xK2thdGtYZFFkNTF0bUpkVUwKaFFSTUhLTTNtc1locEcrRHA3TEVvYmxRbmlsaVRsTWlONndMcjRLa2J4eWRySEdyTnZRWmhKZ0pkb3poQWdxSgppeVdFR1dFeCtQd21KYzNLRVRrQU1oWUQ1d0tCZ1FDNWQ1Vmp5TENEcmE2WjZGdjdYaHBEVTFxSUhod3AxTXNQCkZpVVRVUlpreldHTGVkbk1RQTAwOHRXNU02Ky9MaGNnUkp1UldaUFljaVIzVHNNWE9OUzdiNTgzK3ZicXFQNkcKSSs4TUlaYzE3WUdkUWJXYzBCYUIyamdHUDZQU3NIMit3cVhBLy8zTDl1dUdjVUZUN1ZzSDQyQlhJT3RidnBxNAo0TjUwM2drRzRRS0JnRGJvTk1YNE9UaTVGMjVLLzMvSnZXV3N4Z0c0MHk0U0MvTVNEcVl0NmFpMVJHQWNRaTJoCmxiU2RPVHp6RDM3V3FKcldIZEx1aDBlYWtQR0E4cnJFNFZWSXJhT0M5N0t5Yk5XcExuald3MllRYXg0V3dkR3IKYS82Z0R5b0lQK0VNdHR6azR1YjJKVy9mLzdHRTM0RVg5b3lRd2gzWVJEOEhveFFocHdlZm8wTFJBb0dBTmxVWQo5NDF3WUhMK0JtcHluOVgyZmFpcWlkdkFSbVRuUTdrcURWbWc1TkRoOVpreHU4czcwem9jY0UvNitWZklRSlM0CnVrRHl0ZUxpV2UxQjY4aWpVWEdteENDS096NWNxZkZXODBmWDQxMTdyaFQwM2taN2dYanJGckdJRFkzVW1KQ3YKUERZQ0pNRm1TQkZmb1BXVXlGL005bGxYZVo0Qjk0MHd2aTNabVNFQ2dZQWc4dEVzQ0tKWSswRU5td1ZjOENKaQpPd3VwajI5ckp4eVB5Sll5ZzFqQzllNzJtT3VGNnkyRzhreEJXek1qNUphczBPSFhHWjlweWZTN1JTcHRQMkVXCloySlA3RldaZCtvY1kvc2gzeEY1Njh2QnNnQ0NkS0x2VndTV3FrUk1oMm5TTk1CQUxCd2E1U3dNb0RWanZieUYKRjRJV0hjeXIzZ2hOeXpJRjd1NDJsZz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/Resources/krb5.ini b/Modules/Alkami.DevOps.Minikube/Resources/krb5.ini new file mode 100644 index 0000000..575f4ca --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/Resources/krb5.ini @@ -0,0 +1,34 @@ +[logging] +kdc = SYSDEBUG +kdc = FILE:/var/krb5/log/krb5kdc.log +admin_server = FILE:/var/krb5/log/kadmin.log +default = FILE:/var/krb5/log/krb5lib.log + +[libdefaults] +dns_lookup_realm = true +dns_lookup_kdc = true +forwardable = true +ticket_lifetime = 7d +renew_lifetime = 0 +rdns = false +default_realm = CORP.ALKAMITECH.COM + +[realms] + +CORP.ALKAMITECH.COM = { + kdc = corp.alkamitech.com. + admin_server = CORP.ALKAMITECH.COM + default_domain = CORP.ALKAMITECH.COM +} + +FH.LOCAL = { + kdc = fh.local. + admin_server = FH.LOCAL + default_domain = FH.LOCAL +} + +[domain_realm] +.fh.local = FH.LOCAL +fh.local = FH.LOCAL +.corp.alkamitech.com = CORP.ALKAMITECH.COM +corp.alkamitech.com = CORP.ALKAMITECH.COM \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Minikube/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Minikube/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..74befcf --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/tools/chocolateyInstall.ps1 @@ -0,0 +1,38 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + ## Ensure the module was able to load + Import-Module $id -Global; +} diff --git a/Modules/Alkami.DevOps.Minikube/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Minikube/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..29b2f77 --- /dev/null +++ b/Modules/Alkami.DevOps.Minikube/tools/chocolateyUninstall.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.nuspec b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.nuspec new file mode 100644 index 0000000..6364866 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.nuspec @@ -0,0 +1,37 @@ + + + + Alkami.DevOps.Operations + $version$ + Alkami Platform Modules - DevOps - Operations + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Operations + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps Operations module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.psd1 b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.psd1 new file mode 100644 index 0000000..f1922cd --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.psd1 @@ -0,0 +1,20 @@ +@{ + RootModule = 'Alkami.DevOps.Operations.psm1' + ModuleVersion = '3.27.0' + GUID = '2db01ae4-953d-419a-aef3-d2edab019161' + Author = 'SRE,dsage,cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A set of cmdlets used for day-to-day operations' + FileList = @('Resources\PackageFiltering-min.json', 'Resources\SQL\RadiumPurgeScript.sql', 'Resources\SQL\RadiumPurgeScriptWithJobFilter.sql') + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.IIS','Alkami.PowerShell.Configuration','Alkami.DevOps.Common','Alkami.PowerShell.AD','Alkami.PowerShell.Services','Alkami.PowerShell.Choco' + FunctionsToExport = 'Add-DomainMember','Add-Route53HostedZoneVpcAssociation','Backup-WinTest','Clear-AllRecycleBins','Clear-AwsSqsQueue','Clear-CloudFlareCache','Clear-CloudFlareCacheForSites','Clear-RadiumJobs','Close-TcpSocket','Close-TcpStreamWriter','Convert-CloudFlareARecordsToCnames','Convert-GMSAAccounts','ConvertTo-NormalizedLoadBalancerState','Copy-AlkamiRelease','Copy-LaunchConfiguration','Disable-Microservices','Disable-UnnecessaryMicroservices','Enable-NecessaryMicroservices','Format-IpAddress','Format-SlackMessage','Get-AllLoadBalancerStates','Get-AlwaysEnabledProviderPackageIds','Get-ASInstanceHealth','Get-ASInstanceState','Get-AwsRegionByHostname','Get-BadLogConfiguration','Get-BounceJobBuildId','Get-CloudFlareAuthenticationHeaders','Get-CloudFlareZoneId','Get-DataForSmokeTest','Get-EC2InstancesByHostname','Get-EntrustUserCredentialsFromSecretServer','Get-EnvironmentShortName','Get-LatestOrbAmi','Get-LoadBalancerState','Get-LoadBalancerStateAndHealth','Get-NginxAuthHeaders','Get-NginxHostStates','Get-NginxIpAddresses','Get-NginxUpstreams','Get-OrbAutoScalingGroup','Get-PercentageServerCount','Get-ProviderMappingFilePath','Get-ProviderMappingFileUnion','Get-Route53HostedZoneIdList','Get-Route53HostedZoneTagList','Get-SupportedAwsRegions','Get-TenantTimeZone','Get-VpcIdList','Install-FailedChocoPackages','Install-WinTest','Invoke-RollingScriptBlock','Move-Route53HostedZone','New-Route53HostedZone','New-TcpSocket','New-TcpStreamWriter','Open-ChocolateyLibDir','Open-HostsFile','Open-MachineConfig','Open-NagConfig','Open-PowerShellModulesDir','Ping-EntrustServices','Ping-Host','Push-TcpStreamWriterBuffer','Read-ProviderConfigurationFromTenants','Read-ProviderMappingFile','Read-RadiumPurgeScript','Remove-ADUserProfiles','Repair-LoadBalancerState','Restart-Tier','Select-RunningEC2InstancesByHostname','Send-Counter','Send-DeploymentPackageCounter','Send-DeploymentPackageSizeOrbGauge','Send-DeploymentTypeCounter','Send-DeploymentTypeTimer','Send-Gauge','Send-IncidentSnsMessage','Send-Metric','Send-NewIncidentSnsMessage','Send-ResolveAllIncidentsSnsMessage','Send-Timer','Send-UpdateIncidentSnsMessage','Set-ASInstanceState','Set-EC2InstanceTag','Set-LoadBalancerState','Set-MicroserviceConfigurationBasedState','Set-NagAlerts','Set-NginxHostState','Set-NginxUpstreamHost','Set-Route53HostedZoneVpcAssocations','Set-TracedMessagesEnabled','Show-NetTCPConnections','Test-IsCurrentTimeInsideTenantTimeRange','Test-OpenPorts','Test-SmtpServer','Test-SymConnectNonLoopbackExists','Test-SymitarPorts','Test-TcpConnection','Test-VpcExists','Update-AlkamiDatabase','Update-Database','Update-S3BucketTags','Update-StagedConfigurationValues','Update-TrustedSites','Wait-Route53ChangeStatus','Write-ChocoCommandsToFile','Write-TcpSocketStreamWriter' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'operations') + ProjectUri = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Operations+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Operations+Module' +} diff --git a/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.pssproj b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.pssproj new file mode 100644 index 0000000..3141b5c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Alkami.DevOps.Operations.pssproj @@ -0,0 +1,131 @@ + + + + Debug + 2.0 + {6CAFC0C6-A428-4d30-A9F9-700E829FEA51} + Exe + MyApplication + MyApplication + Alkami.DevOps.Operations + SRE + Alkami Technology + (c) 2017 Alkami Technology. All rights reserved. + A set of cmdlets used for day-to-day operations + 2db01ae4-953d-419a-aef3-d2edab019161 + 0.0.0.1 + + + + + Alkami.DevOps.Common + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Operations") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + Alkami.DevOps.Common + {1bd8fc22-5882-4d5c-8128-81f1d61f8d77} + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/AlkamiManifest.xml b/Modules/Alkami.DevOps.Operations/AlkamiManifest.xml new file mode 100644 index 0000000..d977b10 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Operations + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.ps1 new file mode 100644 index 0000000..be1c216 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.ps1 @@ -0,0 +1,57 @@ +function Get-ADUserProfileList { + <# +.SYNOPSIS + Retrieves an array of CIM instance user profiles on the target server that are in either the CORP or FH domains. + +.SYNOPSIS + Retrieves an array of CIM instance user profiles on the target server that are in either the CORP or FH domains. + +.PARAMETER ComputerName + The name of the server where the operation should be performed. If omitted, defaults to the current host. + +.PARAMETER Domains + Array of domains to process. If omitted, defaults to both CORP and FH. + +.OUTPUTS + An array of CIM instance user profile objects. +#> + [CmdLetBinding()] + [OutputType([object[]])] + param( + [Parameter(Mandatory = $false)] + [string] $ComputerName = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('CORP', 'FH')] + [string[]] $Domains = @('CORP', 'FH') + ) + + $logLead = (Get-LogLeadName) + $result = @() + + # Test if we can get CIM Instances; abort if not. + if ($null -eq (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)) { + Write-Warning "$logLead : Can't CimInstance on this host; aborting." + return $result + } + + # Determine if we are executing remotely. + $splatParams = @{} + if ( -not (Test-StringIsNullOrWhitespace -Value $ComputerName) -and -not (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName )) { + + $splatParams['ComputerName'] = "$ComputerName" + } + + $workingList = Get-CimInstance -ClassName Win32_UserProfile @splatParams + foreach ( $domain in $Domains ) { + + $interim = $workingList | Where-Object { $_.SID.StartsWith($_DomainSidPrefix[$domain]) } + if ( -not (Test-IsCollectionNullOrEmpty -Collection $interim )) { + + Write-Verbose "$logLead : Found $($interim.Length) entries for $domain domain." + $result += $interim + } + } + + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.tests.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.tests.ps1 new file mode 100644 index 0000000..9dcaf4c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileList.tests.ps1 @@ -0,0 +1,135 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" + +InModuleScope -ModuleName Alkami.DevOps.Operations -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $moduleForMock = '' + $inScopeModuleForAssert = 'Alkami.DevOps.Operations' + + Describe 'Get-ADUserProfileList' { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-ADUserProfileList.tests' } + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-CimInstance -ModuleName $moduleForMock -MockWith { + $result = @() + $result += New-Object PSObject @{ SID = "$($_DomainSidPrefix['CORP'])-12345" } + $result += New-Object PSObject @{ SID = "$($_DomainSidPrefix['CORP'])-54321" } + $result += New-Object PSObject @{ SID = "$($_DomainSidPrefix['FH'])-12345" } + $result += New-Object PSObject @{ SID = '1-2-3-4-5-12345' } + + return $result + } + + Context 'Input Validation' { + + It 'Throws if Domain list contains invalid value' { + + { Get-ADUserProfileList -Domains @('Test') } | Should -Throw + } + } + + Context 'Logic' { + + It 'Writes warning if Get-CimInstance is unavailable' { + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $null } + + Get-ADUserProfileList | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Can't CimInstance on this host; aborting." } + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Returns empty list and aborts if Get-CimInstance is unavailable' { + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $null } + + $result = Get-ADUserProfileList + $result | Should -HaveCount 0 + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CimInstance -Times 0 -Exactly -Scope It + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Performs command on localhost by default' { + + Get-ADUserProfileList | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CimInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + } + + It 'Performs command on localhost if ComputerName matches current server' { + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $false } + + $test = 'server1.test.local' + + Get-ADUserProfileList -ComputerName $test | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Value -eq $test } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $test } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CimInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Performs command on remote server if ComputerName does not match current server' { + + $test = 'server2.test.local' + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $false } + + Get-ADUserProfileList -ComputerName $test | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Value -eq $test } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $test } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CimInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $test } + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Filters out non-Active Directory profiles' { + + $results = Get-ADUserProfileList + + foreach ( $result in $results ) { + ($result.SID.StartsWith($_DomainSidPrefix['CORP']) -or $result.SID.StartsWith($_DomainSidPrefix['FH'])) | Should -BeTrue + } + } + + It 'Applies Domain filter if specified' { + + $results = Get-ADUserProfileList -Domains @('CORP') + + foreach ( $result in $results ) { + $result.SID.StartsWith($_DomainSidPrefix['CORP']) | Should -BeTrue + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.ps1 new file mode 100644 index 0000000..75d77bf --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.ps1 @@ -0,0 +1,58 @@ +function Get-ADUserProfileListToRemove { + <# +.SYNOPSIS + Retrieves an array of CIM instance user profiles that can be removed. + +.DESCRIPTION + Retrieves an array of CIM instance user profiles that can be removed. Excludes user profiles + where the name of the folder contains the name of a user who has a running process. Will skip + any profiles whose path contains '$' in an attempt to exclude gMSA accounts. + +.PARAMETER ComputerName + The name of the server where the operation should be performed. If omitted, defaults to the current host. + +.PARAMETER Domains + Array of domains to process. If omitted, defaults to both CORP and FH. + +.OUTPUTS + An array of CIM instance user profile objects. +#> + [CmdLetBinding()] + [OutputType([object[]])] + param( + [Parameter(Mandatory = $false)] + [string] $ComputerName = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('CORP', 'FH')] + [string[]] $Domains = @('CORP', 'FH') + ) + + # Define the hardcoded list of usernames to skip per SYSENG-4133. + $exclusionList = @( + 'appviewx-svc', + 'fh-netwrixmsa', + 'jumpbox.jenkins' + ) + + # Get the AD user profiles. + $result = Get-ADUserProfileList -ComputerName $ComputerName -Domains $Domains + + # Filter out users with active processes. + $activeUsernames = Get-UsernamesWithProcesses -ComputerName $ComputerName + $result = $result | Where-Object { + $username = $_.LocalPath.Split('\')[-1] + return ($activeUsernames -notcontains $username) + } + + # Filter out users in our exclusion list. + $result = $result | Where-Object { + $username = $_.LocalPath.Split('\')[-1] + return ($exclusionList -notcontains $username) + } + + # Filter out users with a '$' in their local path (probably gMSA). + $result = $result | Where-Object { -not $_.LocalPath.Contains('$') } + + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.tests.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.tests.ps1 new file mode 100644 index 0000000..133f4b6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-ADUserProfileListToRemove.tests.ps1 @@ -0,0 +1,88 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" + +InModuleScope -ModuleName Alkami.DevOps.Operations -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $moduleForMock = '' + $inScopeModuleForAssert = 'Alkami.DevOps.Operations' + + Describe 'Get-ADUserProfileListToRemove' { + + Mock -CommandName Get-ADUserProfileList -ModuleName $moduleForMock -MockWith { + $result = @() + $result += New-Object PSObject @{ LocalPath = 'TestDrive:\Test\test1$' } + $result += New-Object PSObject @{ LocalPath = 'TestDrive:\Test\test2' } + $result += New-Object PSObject @{ LocalPath = 'TestDrive:\Test\test3' } + + return $result + } + + Mock -CommandName Get-UsernamesWithProcesses -ModuleName $moduleForMock -MockWith { + return @('test2', 'test4', 'test5') + } + + Context 'Input Validation' { + + It 'Throws if Domain list contains invalid value' { + + { Get-ADUserProfileListToRemove -Domains @('Test') } | Should -Throw + } + } + + Context 'Logic' { + + It 'Performs command on localhost by default' { + + Get-ADUserProfileListToRemove | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileList -Times 1 -Exactly -Scope It ` + -ParameterFilter { [string]::IsNullOrWhiteSpace($ComputerName) } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-UsernamesWithProcesses -Times 1 -Exactly -Scope It ` + -ParameterFilter { [string]::IsNullOrWhiteSpace($ComputerName) } + } + + It 'Uses ComputerName parameter if provided' { + + $test = 'server1.test.local' + + Get-ADUserProfileListToRemove -ComputerName $test | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileList -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $test } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-UsernamesWithProcesses -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $test } + } + + It 'Uses Domain parameter if provided' { + + $test = @('CORP') + Get-ADUserProfileListToRemove -Domains $test | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileList -Times 1 -Exactly -Scope It ` + -ParameterFilter { $null -eq (Compare-Object $Domains $test ) } + } + + It 'Filters out users with running processes' { + + $results = Get-ADUserProfileListToRemove + + foreach ( $result in $results ) { + $result.LocalPath | Should -Not -Match 'test2' + } + } + + It 'Filters out user profiles with a local path containing $' { + + $results = Get-ADUserProfileListToRemove + + foreach ( $result in $results ) { + $result.LocalPath.Contains('$') | Should -BeFalse + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-DecoratedNetTCPConnections.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-DecoratedNetTCPConnections.ps1 new file mode 100644 index 0000000..dc9ea2c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-DecoratedNetTCPConnections.ps1 @@ -0,0 +1,52 @@ +function Get-DecoratedNetTCPConnections { + <# +.SYNOPSIS + Gets Net TCP Connections with Process and Username information + +.PARAMETER UngroupConnections + Do not group by connection's OwningProcess +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias("ShowUngrouped")] + [switch]$UngroupConnections + ) + + $logLead = Get-LogLeadName + + [System.Collections.ArrayList]$connections = Get-NetTCPConnection | Sort-Object -Property OwningProcess + $uniqueProcessIds = $connections | Select-Object -ExpandProperty OwningProcess -Unique + [System.Collections.ArrayList]$matchingProcesses = Get-Process -IncludeUserName -Id $uniqueProcessIds -ErrorAction SilentlyContinue | Sort-Object -Property Id -ErrorAction SilentlyContinue + + if (!($UngroupConnections.IsPresent)) { + + $groupedConnections = $connections | Group-Object -Property OwningProcess + + foreach ($process in $matchingProcesses) { + + $groupedConnections | Where-Object { $_.Name -eq $process.Id } | Add-Member -NotePropertyMembers @{ProcessName = $($process.Name); UserName = $($process.UserName) } + } + + $groupedConnections | Where-Object { $null -eq $_.ProcessName } | ForEach-Object { + + Write-Verbose -Message ("$logLead : Adding Unknown Process and User to Orphaned Process with ID {0}" -f $_.Name) + $_ | Add-Member -NotePropertyMembers @{ProcessName = "Unknown"; UserName = "Unknown" } + } + + $sortedConnections = $groupedConnections | Select-Object -Property Count, ProcessName, UserName, Name, Group | Sort-Object -Property Count -Descending + } else { + + Write-Warning -Message "$logLead : Preparing ungrouped connections. This might take a bit..." + + $ungroupedConnections = $connections + foreach ($process in $matchingProcesses) { + + $ungroupedConnections | Where-Object { $_.OwningProcess -eq $process.Id } | Add-Member -NotePropertyMembers @{ProcessName=$($process.Name);UserName=$($process.UserName)} + } + + $sortedConnections = $ungroupedConnections | Select-Object -Property LocalPort, LocalAddress, RemotePort, RemoteAddress, State, ProcessName, UserName | Sort-Object -Property LocalPort -Descending + } + + return $sortedConnections +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-ELBHealthcheckEndpoints.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-ELBHealthcheckEndpoints.ps1 new file mode 100644 index 0000000..f322ac9 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-ELBHealthcheckEndpoints.ps1 @@ -0,0 +1,68 @@ +function Get-ELBHealthcheckEndpoints { + +<# +.SYNOPSIS + Gets the ELB healthcheck URLs for the server from which it's run. + +.DESCRIPTION + Gets the ELB healthcheck URLs for the server from which it's run. First obtains the name from Get-CurrentInstanceTags, + uses the tag to obtain the ARN, then uses the ARN to obtain the endpoint. + +.EXAMPLE + Get-ELBHealthcheckEndpoints + + https://localhost:8443/IdentityGuardAuthService/services/AuthenticationServiceV11 + https://localhost:8444/IdentityGuardAdminService/services/AdminServiceV11 + +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName) + + Import-AWSModule # ELB2 + + if (!(Test-IsAws)) { + Write-Warning "$logLead : This function can only be executed on an AWS server" + return + } + + # Get the name of the current machine and clean it to match ELB naming convention + $name = (Get-CurrentInstanceTags | Where-Object { $_.Key -eq 'Name' } | Select-Object -First 1).Value + $name = $name -replace '\.','-' + $name = $name -replace '_','-' + $name += '-alb' + Write-Host "$logLead : Name computed as $name" + + # Get the ARN using the name generated above and verify there is only 1 + $endpointARN = Get-ELB2LoadBalancer -Name $name + if (Test-IsCollectionNullOrEmpty -collection $endpointARN) { + + Write-Error "Result returned a NULL or empty value for ARN" + return $null + + } elseif($endpointARN.Count -gt 1) { + + Write-Error "Result returned more than 1 ARN" + return $null + } + + # Get the Endpoint data using the ARN + $elbEndpoints = Get-ELB2TargetGroup -loadbalancerarn $endpointARN.loadbalancerarn + if (Test-IsCollectionNullOrEmpty -collection $elbEndpoints) { + + Write-Error "Result returned NULL or empty value for endpoints" + return $null + } + + # Put together the URLs with the Endpoint data + $results = @() + foreach ($endpoint in $elbEndpoints) { + + $url = '{0}://localhost:{1}{2}' -f $endpoint.Protocol.ToString().ToLower(), $endpoint.Port, $endpoint.healthcheckpath + $results += $url + } + + return $results +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.ps1 new file mode 100644 index 0000000..b634f97 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.ps1 @@ -0,0 +1,56 @@ +function Get-UsernamesWithProcesses { + <# +.SYNOPSIS + Retrieves the unique set of usernames associated with all running processes. + +.SYNOPSIS + Retrieves the unique set of usernames associated with all running processes. This call requires elevated privileges to execute. + +.PARAMETER ComputerName + The name of the server where the operation should be performed. If omitted, defaults to the current host. + +.OUTPUTS + An array of strings containing the usernames of all users with running processes. + +.EXAMPLE + Get-UsernamesWithProcesses + +SYSTEM +njones +LOCAL SERVICE +DWM-2 +UMFD-0 +UMFD-2 +NETWORK SERVICE +#> + [CmdLetBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $false)] + [string] $ComputerName = $null + ) + + $scriptBlock = { + + if ( -not (Test-IsAdmin)) { + throw 'This call requires elevated permissions to run.' + } + + $result = (Get-Process -IncludeUserName).UserName + $result = $result | Where-Object { -not (Test-StringIsNullOrWhitespace -Value $_) } + $result = $result | ForEach-Object { $_.Split('\')[-1] } + $result = $result | Select-Object -Unique + + return $result + } + + # Determine if we are executing remotely. + $splatParams = @{} + if ( -not (Test-StringIsNullOrWhitespace -Value $ComputerName) -and -not (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName )) { + + $splatParams['ComputerName'] = "$ComputerName" + } + + $result = Invoke-Command -ScriptBlock $scriptBlock @splatParams + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.tests.ps1 b/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.tests.ps1 new file mode 100644 index 0000000..56536f6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/Get-UsernamesWithProcesses.tests.ps1 @@ -0,0 +1,86 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +Describe 'Get-UsernamesWithProcesses' { + + $testServer = 'server.test.local' + + Mock -CommandName Test-IsAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return [string]::IsNullOrWhiteSpace($Value) } + Mock -CommandName Invoke-Command -ModuleName $moduleForMock -MockWith { return $ScriptBlock.Invoke() } + + Mock -CommandName Get-Process -ModuleName $moduleForMock -MockWith { + return @( + @{ Username = 'Test\Username' }, + @{ Username = 'Test\Username' }, + @{ Username = $null }, + @{ Username = '' }, + @{ Username = 'Test2\Username' }, + @{ Username = 'Test2\Username2' } + ) + } + + Context 'Logic' { + + It 'Performs command on localhost by default' { + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + + Get-UsernamesWithProcesses | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Process -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return [string]::IsNullOrWhiteSpace($Value) } + } + + It 'Performs command on localhost if ComputerName matches current server' { + + Get-UsernamesWithProcesses -ComputerName $testServer | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Process -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + } + + It 'Performs command on remote server if ComputerName does not match current server' { + + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $false } + + $testServer2 = 'server2.test.local' + + Get-UsernamesWithProcesses -ComputerName $testServer2 | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Process -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer2 } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $testServer2 } + + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Returns unique usernames only' { + + $result = Get-UsernamesWithProcesses + $result | Should -HaveCount 2 + $result | Should -Contain 'Username' + $result | Should -Contain 'Username2' + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Process -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.Operations/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..5f09ec9 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Private/VariableDeclarations.ps1 @@ -0,0 +1,4 @@ +$_DomainSidPrefix = @{ + CORP = 'S-1-5-21-1114677349-1136464307-1343122729' + FH = 'S-1-5-21-977684154-3037035785-3226355436' +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.ps1 b/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.ps1 new file mode 100644 index 0000000..a9eca3a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.ps1 @@ -0,0 +1,177 @@ +function Add-DomainMember { + <# +.SYNOPSIS + Adds the current machine to the specified domain, and changes the computer name to the user supplied value or value of the alk:hostname tag + +.DESCRIPTION + Adds the current machine to the specified domain, and changes the computer name to the user supplied value or value of the alk:hostname tag + +.PARAMETER Cred + [PSCredential] Pass a credential object to capture the credentials before running the command. + The function will automatically prompt you for the domain credentials when you run it. + +.PARAMETER OUPath + [string] The path to the OU that the computer needs to be added to. + +.PARAMETER Domain + [string] The name of the domain that you want to join the machine to. + +.PARAMETER NewHostName + [string] The new name of the target machine, this will also be the name added to Active Directory. + This parameter is not mandatory. If this parameter is not provided, the function will look for the alk:hostname AWS tag. + If it cannot find either the parameter or the AWS tag the function will fail. + +.PARAMETER SecurityGroupNames + [string[]] The names of the AD security groups to assign to the machine. + This parameter is not mandatory. If this parameter is not provided, the machine will not be added to any Security Groups when joined to the domain. + +.PARAMETER NoRestart + [switch] Flag indicating that the computer should not restart. + Note: On EC2 instances with EC2 Launch V2, this flag should be set if calling this function from the userdata script. + Ref: https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2launch-v2-settings.html#ec2launch-v2-exit-codes-reboots + +.Example + Add-DomainMember -NewHostName "app1637199" -Domain "fh.local" -SecurityGroupNames @("WSUS-Servers-9pm") -OUPath "OU=POD19,OU=FH Computers,DC=fh,DC=local" +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCredential] $Cred, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$OUPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Domain, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$SecurityGroupNames = $null, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$NewHostName = $null, + + [Parameter(Mandatory = $false)] + [switch]$NoRestart + ) + + $logLead = (Get-LogLeadName) + Import-AWSModule + + if ($true -eq (Get-CimInstance Win32_ComputerSystem).PartOfDomain) { + + Write-Warning "$logLead : This computer is already domain joined." + return + } + + # Find the PDC in the target domain. + [string] $pdcServer = (Get-ADDomain -Credential $Cred -Server $Domain).PDCEmulator + if ( [string]::IsNullOrWhitespace( $pdcServer )) { + + Write-Error "$logLead : Unable to find the primary domain controller in the target domain." + return + } + + $secGroupIdentities = @() + if ( $PSBoundParameters.ContainsKey( 'SecurityGroupNames' )) { + + foreach ( $securityGroup in $SecurityGroupNames ) { + + Write-Verbose "$logLead : Determining if security group '$securityGroup' exists on domain '$Domain'" + + $curSecGroup = Get-ADGroup -Filter { GroupCategory -eq 'Security' -and Name -eq $securityGroup } -Server $pdcServer -Credential $Cred + if ( $null -eq $curSecGroup ) { + + Write-Error "$logLead : Security Group $securityGroup not found; aborting." + return + + } else { + + $secGroupIdentities += $curSecGroup + } + } + } + + if ($false -eq $PSBoundParameters.ContainsKey( 'NewHostName' )) { + + try { + + $hostnameTag = Get-InstanceHostname (Get-CurrentInstance) + + } catch { + + Write-Warning "$logLead : Get-InstanceHostname threw exception : $($_.Exception.Message)" + $hostnameTag = $null + } + + if ( [string]::IsNullOrEmpty( $hostnameTag )) { + + Write-Error "$logLead : Hostname was not provided and could not be read from tags; aborting." + return + + } else { + + Write-Verbose "$logLead : Using computer name from tags: $hostnameTag" + $alkName = $hostnameTag + } + + } else { + + Write-Verbose "$logLead : Using computer name from user input: $NewHostName" + $alkName = $NewHostName + } + + $currentHostname = (Get-FullyQualifiedServerName).Split('.')[0] + if ( $currentHostname -ne $alkName ) { + + Write-Host "$logLead : Renaming computer to $alkName" + Rename-Computer -ComputerName $env:COMPUTERNAME -NewName $alkName -Force + Start-Sleep -s 10 + + } else { + + Write-Host "$logLead : Skipping computer rename; computer is already named $alkName" + } + + Add-Computer -DomainName $Domain -OUPath "$OUPath" -Server $pdcServer -Credential $Cred -Options JoinWithNewName, AccountCreate + + # Determine if there are security groups to add to the computer. + if ( $false -eq ( Test-IsCollectionNullOrEmpty $secGroupIdentities )) { + + # Test to see if the computer object exists. We cannot attach security groups until it does. + $adComputerTestBlock = { + + param([string] $hostname, [string] $dc, [PSCredential] $cred) + return (Get-ADComputer $hostname -Server $dc -Credential $cred) + } + + # We have to use the NETBIOS name which clips at 15 characters. + $netbiosName = $alkName.Substring(0, [System.Math]::Min(15, $alkName.Length)) + $testResult = Invoke-CommandWithRetry -ScriptBlock $adComputerTestBlock -Arguments @($netbiosName, $pdcServer, $Cred) -Seconds 5 -MaxRetries 12 -ErrorAction SilentlyContinue + if ( $null -eq $testResult) { + + Write-Warning "$logLead : Skipping security groups because the domain controller did not report existence of the computer object." + + } else { + + foreach ( $secGroupIdentity in $secGroupIdentities ) { + + Write-Host "$logLead : Adding Machine to the $($secGroupIdentity.Name) Security Group" + Add-ADGroupMember -Identity $secGroupIdentity -Members $testResult -Server $pdcServer -Credential $Cred + } + } + } + + if ( $NoRestart ) { + + Write-Warning "$logLead : Skipping restart of instance at user request. A restart is required after joining the domain." + + } else { + + Restart-Computer -Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.tests.ps1 new file mode 100644 index 0000000..6c2044f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Add-DomainMember.tests.ps1 @@ -0,0 +1,272 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +# Define fake PSCredential user. +$user = 'AlkamiFakeUser' +$pass = Get-SecureString 'abc123' +$testCredential = New-Object System.Management.Automation.PSCredential($user, $pass) + +Import-AWSModule + +Describe 'Add-DomainMember' { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Add-DomainMember.tests' } + Mock -CommandName Get-FullyQualifiedServerName -ModuleName $moduleForMock -MockWith { return 'Test.domain.test' } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-CurrentInstance -ModuleName $moduleForMock -MockWith { return New-Object Amazon.EC2.Model.Instance } + Mock -CommandName Get-ADDomain -ModuleName $moduleForMock -MockWith { return @{PDCEmulator = '1.2.3.4' } } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADGroup } + Mock -CommandName Invoke-CommandWithRetry -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADComputer } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { return 'AlkHostnameTag' } + + Context 'Input Validation' { + + It 'OUPath Should Not Be Null' { + { Add-DomainMember -Cred $testCredential -OUPath $null } | Should -Throw + } + + It 'OUPath Should Not Be Empty' { + { Add-DomainMember -Cred $testCredential -OUPath '' } | Should -Throw + } + + It 'Domain Should Not Be Null' { + { Add-DomainMember -Cred $testCredential -Domain $null -OUPath $null } | Should -Throw + } + + It 'Domain Should Not Be Empty' { + { Add-DomainMember -Cred $testCredential -Domain '' -OUPath '' } | Should -Throw + } + } + + Context 'Result Validation' { + + Mock -CommandName Get-CimInstance -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Rename-Computer -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Start-Sleep -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Add-Computer -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Add-ADGroupMember -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Restart-Computer -ModuleName $moduleForMock -MockWith {} + + It 'Should Return With Warning If Domain Joined' { + + Mock -CommandName Get-CimInstance -ModuleName $moduleForMock -MockWith { return New-Object psobject -Property @{PartOfDomain = $true } } + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' + + Assert-MockCalled -CommandName Get-CimInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'This computer is already domain joined' } + + Mock -CommandName Get-CimInstance -ModuleName $moduleForMock -MockWith {} + } + + It 'Should Return With Error If No Domain Controller IP Address Is Found' { + + Mock -CommandName Get-ADDomain -ModuleName $moduleForMock -MockWith { return $null } + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Bad' ) + + Assert-MockCalled -CommandName Get-ADDomain -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Unable to find the primary domain controller in the target domain' } + + Mock -CommandName Get-ADDomain -ModuleName $moduleForMock -MockWith { return @{PDCEmulator = '1.2.3.4' } } + } + + It 'Should Return With Error If Invalid Security Group Name Is Provided' { + + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Bad' ) + + Assert-MockCalled -CommandName Get-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Security Group Bad not found; aborting' } + + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADGroup } + } + + It 'Should Return With Error If Hostname tag is Empty and Value not provided as parameter' { + + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { throw 'Hostname Tag is Empty' } + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' + + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-InstanceHostname -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-CurrentInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Get-InstanceHostname threw exception : Hostname Tag is Empty' } + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Hostname was not provided and could not be read from tags; aborting' } + + Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith { return 'AlkHostnameTag' } + } + + It 'Should use alk:hostname tag if parameter is not provided' { + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' + + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-InstanceHostname -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-CurrentInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $NewName -match 'AlkHostnameTag' } + Assert-MockCalled -CommandName Start-Sleep -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should use hostname parameter if provided' { + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -NewHostName 'HostnameParameter' + + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-InstanceHostname -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-CurrentInstance -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $NewName -match 'HostnameParameter' } + Assert-MockCalled -CommandName Start-Sleep -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should Skip Host Rename If Computer Name Matches Target Value' { + + Add-DomainMember -Cred $testCredential -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -NewHostName 'Test' + + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-InstanceHostname -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-CurrentInstance -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should Add Computer to All Specified Security Groups' { + Add-DomainMember -Cred $testCredential -NewHostName 'Test' -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Test1', 'Test2', 'Test3' ) + + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Rename-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Invoke-CommandWithRetry -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Restart-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should Skip Security Group processing if No Security Groups Were Provided' { + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + + Add-DomainMember -Cred $testCredential -NewHostName 'Test' -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' + + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Invoke-CommandWithRetry -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + } + + It 'Should Skip Security Group processing if ADComputer object is Not Detected' { + + Mock -CommandName Invoke-CommandWithRetry -ModuleName $moduleForMock -MockWith { return $null } + + Add-DomainMember -Cred $testCredential -NewHostName 'Test' -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Test1', 'Test2', 'Test3' ) + + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Invoke-CommandWithRetry -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + + Mock -CommandName Invoke-CommandWithRetry -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADComputer } + } + + It 'Should Write a Warning if ADComputer object is Not Detected' { + + Mock -CommandName Invoke-CommandWithRetry -ModuleName $moduleForMock -MockWith { return $null } + + Add-DomainMember -Cred $testCredential -NewHostName 'Test' -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Test1', 'Test2', 'Test3' ) + + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Invoke-CommandWithRetry -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Skipping security groups because the domain controller did not report existence of the computer object' } + + Mock -CommandName Invoke-CommandWithRetry -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADComputer } + } + + It 'Should Target the Same Domain Controller for All Active Directory Operations' { + Add-DomainMember -Cred $testCredential -NewHostName 'Test' -Domain 'Test' -OUPath 'ou=bar,ou=foo,dc=test,dc=local' -SecurityGroupNames @( 'Test1', 'Test2', 'Test3' ) + + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Server -match '1.2.3.4' } + Assert-MockCalled -CommandName Add-Computer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Server -match '1.2.3.4' } + Assert-MockCalled -CommandName Add-ADGroupMember -Times 3 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Server -match '1.2.3.4' } + # Because Get-ADComputer is wrapped by Invoke-CommandWithRetry, we can't test that one . . . + } + + It 'Should Skip Restart If NoRestart Flag Is Provided' { + + Add-DomainMember -Cred $testCredential ` + -NewHostName 'Test' ` + -Domain 'Test' ` + -OUPath 'ou=bar,ou=foo,dc=test,dc=local' ` + -SecurityGroupNames @( 'Test1', 'Test2', 'Test3' ) ` + -NoRestart + + Assert-MockCalled -CommandName Restart-Computer -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match 'Skipping restart of instance at user request' } + } + } +} + +Remove-Module -Name AWSPowerShell diff --git a/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.ps1 b/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.ps1 new file mode 100644 index 0000000..261d63a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.ps1 @@ -0,0 +1,96 @@ +function Add-Route53HostedZoneVpcAssociation { + +<# +.SYNOPSIS + Adds VPC associations to an existing Route53 Hosted Zone. + +.DESCRIPTION + Adds VPC associations to an existing Route53 Hosted Zone. Refer to https://aws.amazon.com/premiumsupport/knowledge-center/private-hosted-zone-different-account/. + +.PARAMETER ZoneId + [string] The ID of the Route53 Hosted Zone to update. + +.PARAMETER ZoneProfileName + [string] The AWS profile where the Route53 Hosted Zone exists (e.g. 'temp-prod'). + +.PARAMETER VpcId + [string] The ID of the VPC to associate with the Route53 Hosted Zone. + +.PARAMETER VpcRegion + [string] The AWS region of the VPC to associate with the Route53 Hosted Zone. + +.PARAMETER VpcProfileName + [string] The AWS profile where the VPC rcists (e.g. 'temp-prod'). + +.EXAMPLE + Add-Route53HostedZoneVpcAssociation -ZoneId 'ZONE9999' -ZoneProfileName 'temp-prod' -VpcId 'vpc-1234' -VpcRegion 'us-east-1' -VpcProfileName 'temp-prod' + +[Add-Route53HostedZoneVpcAssociation]: Creating VPC 'vpc-1234' association authorization for Route53 Hosted Zone 'ZONE9999' +[Add-Route53HostedZoneVpcAssociation]: Creating VPC 'vpc-1234' association for Route53 Hosted Zone 'ZONE9999' +[Add-Route53HostedZoneVpcAssociation]: Removing VPC 'vpc-1234' association authorization for Route53 Hosted Zone 'ZONE9999' +#> + + [OutputType([Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ZoneId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ZoneProfileName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $VpcId, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $VpcRegion, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $VpcProfileName + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule + + # Step 1: Validate the Zone ID exists using the provided information. + $targetZone = ( Get-R53HostedZone -Id $ZoneId -ProfileName $ZoneProfileName ) + if ( $null -eq $targetZone ) { + + Write-Error "$logLead : Route53 Hosted Zone ID '$ZoneId' not found using profile '$ZoneProfileName'." + return $false + } + + # Step 2: Validate the VPC exists using the provided information. + if ( $false -eq (Test-VpcExists -Id $VpcId -ProfileName $VpcProfileName -Region $VpcRegion)) { + + Write-Error "$logLead : VPC ID '$VpcId' in region '$VpcRegion' not found using profile '$ZoneProfileName'." + return $false + } + + # Step 3: Validate that the VPC is not already associated with the Route53 Hosted Zone (aka: "No-Op"). + $vpcExists = $targetZone.VPCs | Where-Object { $_.VPCId -eq $VpcId } + if ( $null -ne $vpcExists ) { + Write-Host "$logLead : VPC ID '$VpcId' already associated with Route53 Hosted Zone ID '$ZoneId'." + return $true + } + + # Step 4: Authorize the VPC association (using the Hosted Zone profile). + Write-Host ( "{0}: Creating VPC '{1}' association authorization for Route53 Hosted Zone '{2}'" -f $logLead, $VpcId, $ZoneId ) + New-R53VPCAssociationAuthorization -HostedZoneId $ZoneId -VPC_VPCId $VpcId -VPC_VPCRegion $VpcRegion -ProfileName $ZoneProfileName | Out-Null + + # Step 5: Create the VPC association (using the VPC profile). + Write-Host ( "{0}: Creating VPC '{1}' association for Route53 Hosted Zone '{2}'" -f $logLead, $VpcId, $ZoneId ) + $changeOutput = Register-R53VPCWithHostedZone -HostedZoneId $ZoneId -VPC_VPCId $VpcId -VPC_VPCRegion $VpcRegion -ProfileName $VpcProfileName + Wait-Route53ChangeStatus -ChangeId $changeOutput.Id -AwsProfileName $ZoneProfileName + + # Step 6: Remove the VPC association authorization (using the Hosted Zone profile). + Write-Host ( "{0}: Removing VPC '{1}' association authorization for Route53 Hosted Zone '{2}'" -f $logLead, $VpcId, $ZoneId ) + Remove-R53VPCAssociationAuthorization -HostedZoneId $ZoneId -VPC_VPCId $VpcId -VPC_VPCRegion $VpcRegion -ProfileName $ZoneProfileName -Force | Out-Null + return $true +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.tests.ps1 new file mode 100644 index 0000000..8b2dccb --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Add-Route53HostedZoneVpcAssociation.tests.ps1 @@ -0,0 +1,153 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Add-Route53HostedZoneVpcAssociation" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { + return @( + @{ 'Region' = 'us-east-1' }, + @{ 'Region' = 'us-west-2' } + ) + } + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Add-Route53HostedZoneVpcAssociation.tests' } + + Context "Input Validation" { + + It "Zone ID Should Not Be Null" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId $null } | Should -Throw + } + + It "Zone ID Should Not Be Empty" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId '' } | Should -Throw + } + + It "Zone Profile Name Should Not Be Null" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName $null } | Should -Throw + } + + It "Zone Profile Name Should Not Be Empty" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName '' } | Should -Throw + } + + It "VPC ID Should Not Be Null" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId $null } | Should -Throw + } + + It "VPC ID Should Not Be Empty" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId '' } | Should -Throw + } + + It "VPC Region Should Not Be Null" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion $null } | Should -Throw + } + + It "VPC Region Should Not Be Empty" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion '' } | Should -Throw + } + + It "VPC Region Should Be In Supported Regions List" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'Test' } | Should -Throw + } + + It "VPC Profile Name Should Not Be Null" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'us-east-1' -VpcProfileName $null } | Should -Throw + } + + It "VPC Profile Name Should Not Be Empty" { + + { Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'us-east-1' -VpcProfileName '' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Return With Error If Hosted Zone Does Not Exist" { + + Mock -CommandName Get-R53HostedZone -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'us-east-1' -VpcProfileName 'Test' + $result | Should -BeFalse + + Assert-MockCalled -CommandName Get-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Route53 Hosted Zone ID .* not found" } + } + + It "Should Return With Error If VPC Does Not Exist" { + + Mock -CommandName Get-R53HostedZone -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'us-east-1' -VpcProfileName 'Test' + $result | Should -BeFalse + + Assert-MockCalled -CommandName Get-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "VPC ID .* in region .* not found" } + } + + It "Should Return Success With No Actions Taken When VPC Association Already Exists On Route53 Hosted Zone" { + + Mock -CommandName Get-R53HostedZone -ModuleName $moduleForMock -MockWith { @{ "VPCs" = @( @{ 'VPCId' = 'Test'})} } + Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName New-R53VPCAssociationAuthorization -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Register-R53VPCWithHostedZone -ModuleName $moduleForMock -MockWith { @{ 'ID' = 'Test'} } + Mock -CommandName Wait-Route53ChangeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Remove-R53VPCAssociationAuthorization -ModuleName $moduleForMock -MockWith {} + + $result = Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'Test' -VpcRegion 'us-east-1' -VpcProfileName 'Test' + $result | Should -BeTrue + + Assert-MockCalled -CommandName Get-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-R53VPCAssociationAuthorization -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Register-R53VPCWithHostedZone -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Wait-Route53ChangeStatus -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Remove-R53VPCAssociationAuthorization -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Return Success When VPC Association Created On Route53 Hosted Zone" { + + Mock -CommandName Get-R53HostedZone -ModuleName $moduleForMock -MockWith { @{ "VPCs" = @( @{ 'VPCId' = 'Test'})} } + Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName New-R53VPCAssociationAuthorization -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Register-R53VPCWithHostedZone -ModuleName $moduleForMock -MockWith { @{ 'ID' = 'Test'} } + Mock -CommandName Wait-Route53ChangeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Remove-R53VPCAssociationAuthorization -ModuleName $moduleForMock -MockWith {} + + $result = Add-Route53HostedZoneVpcAssociation -ZoneId 'Test' -ZoneProfileName 'Test' -VpcId 'FullTest' -VpcRegion 'us-east-1' -VpcProfileName 'Test' + $result | Should -BeTrue + + Assert-MockCalled -CommandName Get-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-R53VPCAssociationAuthorization -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Register-R53VPCWithHostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Wait-Route53ChangeStatus -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Remove-R53VPCAssociationAuthorization -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Backup-WinTest.ps1 b/Modules/Alkami.DevOps.Operations/Public/Backup-WinTest.ps1 new file mode 100644 index 0000000..c3831c6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Backup-WinTest.ps1 @@ -0,0 +1,35 @@ +function Backup-WinTest { +<# +.SYNOPSIS + Copy C:\Tools\WinTest\Current\* to C:\Tools\WinTest\{DATETIMESTAMP} folder + Returns full path to newly backed up folder +#> + + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$sourcePath = "c:\Tools\WinTest\Current", + [Parameter(Mandatory=$false)] + [string]$backupPath = "c:\Tools\WinTest\" + ) + + $logLead = (Get-LogLeadName); + $datetime = Get-Date -Format "yyyyMMddHHmm" + $backupFolder = Join-Path -Path $backupPath -ChildPath $datetime + + if (Test-Path -Path $sourcePath) { + if (Test-Path -Path $backupFolder) { + Write-Warning ("$logLead : Backup folder already exists at {0} `nNo backup performed" -f $backupFolder) + return $null + } + Write-Verbose ("$logLead : Renaming {0} to {1}" -f $sourcePath, $backupFolder) + Rename-Item -Path $sourcePath -NewName $datetime + Write-Verbose ("$logLead : Previous WinTest backed up to {0}" -f $backupFolder) + return $backupFolder + } else { + Write-Verbose "$logLead : WinTest Current folder does not exist, no backup performed" + return $null + } + +} + diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-AllRecycleBins.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-AllRecycleBins.ps1 new file mode 100644 index 0000000..4155bdd --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-AllRecycleBins.ps1 @@ -0,0 +1,95 @@ +function Clear-AllRecycleBins { +<# +.SYNOPSIS + Clears the recycle bin for all users on the current server + +.PARAMETER CreateScheduledTask + [switch] If passed to the command, will create and register + a scheduled task to run every Saturday weekly to clear all recycle bins + if one doesn't already exist, otherwise, skip +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [switch]$CreateScheduledTask + ) + + $logLead = Get-LogLeadName + + if (Test-Path "C:\`$Recycle.Bin") { + + Write-Verbose "$logLead : Looking for symlinks in recycle bins" + $symLinks = Get-ChildItem "C:\`$Recycle.bin" -Recurse -Force -ErrorAction "SilentlyContinue" | Where-Object {$_.PSIsContainer -and $_.Attributes.ToString() -match "ReparsePoint"} + + if ($null -ne $symLinks) { + + Write-Host "$logLead : Symlinks found. Deleting them." + $symLinks | ForEach-Object { + Write-Host "$logLead : Removing Symlink $($_.FullName)" + (Get-Item $_.FullName).Delete() + } + } + + $itemsToDelete = Get-ChildItem "C:\`$Recycle.bin" -Force -Directory + $totalCleared = 0 + + foreach ($binToDelete in $itemsToDelete) { + + $userSID = $binToDelete.Name + + try { + + $userName = Get-UsernameFromSid -SecurityIdentifier $userSID -ErrorAction SilentlyContinue + + } catch { + + $userName = "UNKNOWN" + } + + $binLength = 0 + Get-ChildItem $binToDelete.FullName -Force -Recurse | ForEach-Object { $binLength += $_.Length} + $totalCleared += $binLength + + $roundedData = $([math]::Round(($binLength/1024/1024),2)) + + if ($roundedData -eq 0) { + + Write-Host "$logLead : User [$userName] has negligable data in the recycling bin which will be removed." + + } else { + + Write-Host "$logLead : User [$userName] has $($roundedData)mb of data in the recycling bin which will be removed." + } + + Remove-Item $binToDelete.FullName -Force -Recurse + } + + Write-Host "$logLead : Deletes finished. Number of items deleted: $($itemsToDelete.Count) totalling $([math]::Round(($totalCleared/1024/1024),2))mb" + } else { + + Write-Host "$logLead : Recycle Bin does not exist. Skipping Recycle Bin clear." + } + + if ($createScheduledTask.IsPresent) { + + $taskName = "Clear All Recycle Bins" + $taskArray = ScheduledTasks\Get-ScheduledTask | Select-Object -Property TaskName + + $recycleBinTask = $taskArray | Where-Object { $_.TaskName -eq $taskName } + $taskExists = $null -ne $recycleBinTask + + if (!($taskExists)) { + + $actionArgument = '-WindowStyle Hidden -command "&{Clear-AllRecycleBins}"' + $registerDescription = "Clears the recycle bin of all users on the current server weekly at 9AM." + + $action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument $actionArgument + $trigger = New-ScheduledTaskTrigger -Weekly -At 9am -DaysOfWeek "Saturday" + + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $taskName -Description $registerDescription -User "system" -RunLevel "Highest" + } else { + + Write-Host "$logLead : The recycle bin task already exists. Skipping creation of a new one." + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.ps1 new file mode 100644 index 0000000..35aa650 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.ps1 @@ -0,0 +1,236 @@ +function Clear-AwsSqsQueue { + + <# + .SYNOPSIS + Clears all messages from AWS Queues specified by the user. + + .DESCRIPTION + Clears all messages from AWS Queues specified by the user. Supports exact match, queue prefix, and dynamically matched queues + using machine.config settings + + .PARAMETER QueueName + A specific SQS queue name. When supplied, only this queue will be cleared, and only if there is an exact match on Queue Name + + .PARAMETER QueuePrefix + A prefix for queues. When supplied, all queues with this name prefix will be cleared + + .PARAMETER AllMatchedQueues + Dynamically determines the appropriate queue name format using local machine data. Will only work on an application server + + .PARAMETER IncludeDeadLetterQueues + By default, dead letter queues are excluded. When this flag is supplied, dead letter queues will also be cleared + + .PARAMETER Force + When supplied, bypasses all confirmations. Typically not recommended to do so but it's best practice to include + + .LINK + Runbook: https://confluence.alkami.com/x/B71iDg + + .EXAMPLE + Clear-AwsSqsQueue -QueueName "Qa-trin-AlkamiMSBACHOptInAlertsHost" -Region us-east-1 -Profile temp-qa +[Clear-AwsSqsQueue] : Searching for SQS Queues Using Queue Prefix: [Qa-trin-AlkamiMSBACHOptInAlertsHost] +[Clear-AwsSqsQueue] : Found 1 queue(s) to purge based on user input. +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiMSBACHOptInAlertsHost] + + .EXAMPLE + Clear-AwsSqsQueue -QueuePrefix "Qa-trin-" -Region us-east-1 -Profile temp-qa +[Clear-AwsSqsQueue] : Searching for SQS Queues Using Queue Prefix: [Qa-trin-] +[Clear-AwsSqsQueue] : Found 7 queue(s) to purge based on user input. + +Large Queue Count Purge +7 SQS queues will be purged. Are you sure you want to continue? +[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): y +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiAppRadiumWindowsService-Accounts] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiAppRadiumWindowsService-Transactions] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiAppRadiumWindowsService.fifo] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiMSAccountRefreshSubscriptionFilterHost] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiMSAlertSubscriptionChangedConsumerCOCCHost] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiMSAuthenticationWorkflowHost-UsernameChanged] +[Clear-AwsSqsQueue] : Purging Messages from Queue: [https://sqs.us-east-1.amazonaws.com/668894625708/Qa-trin-AlkamiMSAutomaticDepositAlertHost] + #> + + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true, ParameterSetName = "QueueName")] + [string]$QueueName, + + [Parameter(Mandatory = $true, ParameterSetName = "QueuePrefix")] + [string]$QueuePrefix, + + [Parameter(Mandatory = $true, ParameterSetName = "AllMatchedQueues")] + [switch]$AllMatchedQueues, + + [Parameter(Mandatory = $false, ParameterSetName = "__AllParameterSets")] + [switch]$IncludeDeadLetterQueues, + + [Parameter(Mandatory = $false, ParameterSetName = "__AllParameterSets")] + [switch]$Force + ) + DynamicParam { + + # Adds Dynamic Parameters for AWS Region and ProfileName + Get-AwsStandardDynamicParameters + } + + begin { + + Import-AWSModule + $logLead = Get-LogLeadName + + $parameterSet = $PSCmdlet.ParameterSetName + $isRunningInAws = Test-IsAws + + $awsSplatParams = @{} + $nextToken = "" + $queueSearchPrefix = "" + + $exactMatchOnly = ("QueueName" -eq $parameterSet) + + $Region = $PsBoundParameters["Region"] + $ProfileName = $PsBoundParameters["ProfileName"] + } + + process { + + # Identify the Appropriate Target Region + if (Test-StringIsNullOrEmpty -Value $Region) { + + if ($isRunningInAws) { + + # Use the Current Instances Region + $awsSplatParams["Region"] = Get-CurrentInstanceRegion + + } else { + + # Cannot Continue + Write-Warning "$logLead : No region was supplied and this command is not running in AWS. Execution cannot continue" + return + } + } else { + + # Use the User Supplied Region + $awsSplatParams["Region"] = "$Region" + } + + # Identify How to Authenticate Our Calls + if (Test-StringIsNullOrEmpty $ProfileName) { + + if ($isRunningInAws) { + + # Use the IAM Instance Profile + Write-Verbose "$logLead : No profile was supplied to the function. The IAM instance profile will be used in lieu." + + } else { + + # Cannot Continue + Write-Warning "$logLead : No profile was supplied and this command is not running in AWS. Execution cannot continue." + return + } + } else { + + # Use the User Supplied Profile Name + $awsSplatParams["ProfileName"] = "$ProfileName" + } + + # Deal with calculated the queue search prefix if needed + if ("AllMatchedQueues" -eq $parameterSet) { + + if (-NOT $isRunningInAws) { + + Write-Warning "$logLead : No search parameters were supplied, and calculated queue names are only available in AWS. Execution cannot continue" + return + } + + # Dynamically figure out what queues to purge based on values from the machine.config + $environmentType = Get-EnvironmentType + $designation = (Get-EnvironmentNameSafeDesignation).ToLowerInvariant() + + if (Test-StringIsNullOrEmpty -Value $environmentType) { + + Write-Warning "$logLead : Could not read the environment type from the current machine.config. Execution cannot continue." + return + } + + if (Test-StringIsNullOrEmpty -Value $designation) { + + Write-Warning "$logLead : Could not read the name safe desgination from the current machine.config. Execution cannot continue." + return + } + + $queueSearchPrefix = $environmentType + "-" + $designation + "-" + Write-Verbose "$logLead : No queue name or prefix supplied. Calculated search prefix: $queueSearchPrefix" + + } else { + + # Otherwise we'll just search for what they asked for + $queueSearchPrefix = Get-CoalescedStringValue -ValueA $QueueName -ValueB $QueuePrefix + } + + Write-Host "$logLead : Searching for SQS Queues Using Queue Prefix: [$queueSearchPrefix]" + $queueSearchResult = Get-SQSQueue -MaxResult 1000 -Select "*" -QueueNamePrefix $queueSearchPrefix @awsSplatParams + [array]$targetQueues = $queueSearchResult.QueueUrls + + if (Test-IsCollectionNullOrEmpty -Collection $targetQueues) { + + Write-Warning "$logLead : No matching queues found." + return + } + + $nextToken = $queueSearchResult.NextToken + + # In their infinite wisdom, AWS limits the number of queues that may be returned to no more than 1000 at a time + # This handles that + # Though really, should anyone be trying to purge more than 1000 queues at a time? + # Dear reader, not my problem. We will prompt for confirmation if more than 5 queues are selected for purging later on. + # ToDo: Move this in to a Get-AllSQSQueues type function + if (-NOT (Test-StringIsNullOrEmpty $nextToken)) { + + do { + Write-Verbose "$logLead : Searching for queues using next token due to batching limits" + $queueSearchResult = Get-SQSQueue -MaxResult 1000 -Select "*" -QueueNamePrefix $queueSearchPrefix -NextToken $nextToken @awsSplatParams + + $targetQueues += $queueSearchResult.QueueUrls + $nextToken = $queueSearchResult.NextToken + + } while (-NOT (Test-StringIsNullOrEmpty -Value $nextToken)) + } + + # Remove all Dead Letter Queues by Default + if (-NOT ($IncludeDeadLetterQueues.IsPresent)) { + + Write-Verbose "$logLead : Removing dead letter queues from targets" + $targetQueues = $targetQueues | Where-Object {$_ -notmatch "-Dead$"} + } + + # Only use Exact Matches if a Queue Name Was Provided + if ($exactMatchOnly) { + + $targetQueues = $targetQueues | Where-Object {$_ -match "/$QueueName$"} + } + + $queuesCount = $targetQueues.Count + Write-Host "$logLead : Found $queuesCount queue(s) to purge based on user input." + + if ($queuesCount -gt 5 -and (-NOT ($Force.IsPresent))) { + + $continuePrompt = "$queuesCount SQS queues will be purged. Are you sure you want to continue?" + [bool]$yesToAll = $false + [bool]$noToAll = $false + if (-NOT (Test-ShouldContinue -Query $continuePrompt -Caption "Large Queue Count Purge" -YesToAll ([ref]$yesToAll) -NoToAll ([ref]$noToAll)) -or $noToAll) { + + Write-Warning "$logLead : User aborted process. Purge will not be performed." + return + } + } + + foreach ($queue in $targetQueues) { + + $purgeMessage = "Purging Messages from Queue: [$queue]" + if ($yesToAll -or $Force.IsPresent -or (Test-ShouldProcess -Target $queue -Action "Purging SQS Queue")) { + + Write-Host "$logLead : $purgeMessage" + Clear-SQSQueue -QueueUrl $queue @awsSplatParams + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.tests.ps1 new file mode 100644 index 0000000..ff2f82b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-AwsSqsQueue.tests.ps1 @@ -0,0 +1,301 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Clear-AwsSqsQueue" { + + Mock -CommandName Clear-SQSQueue -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AwsStandardDynamicParameters -ModuleName $moduleForMock -MockWith { + + # Assign fake/safe values here + $dynamicParams = @( + @{ Name = "Region"; Values = @( "canada-east-99","antartica-south-1" ); }, + @{ Name = "ProfileName"; Values = @( "bippity-boppity","boo" ) } + ) + + $runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + foreach ($param in $dynamicParams) { + + $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $parameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute + $parameterAttribute.Mandatory = $false + $parameterAttribute.ParameterSetName = "__AllParameterSets" + $attributeCollection.Add($parameterAttribute) + $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($param.Values) + $attributeCollection.Add($validateSetAttribute) + + $runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($param.Name, [string], $attributeCollection) + $runtimeParameterDictionary.Add($param.Name, $RuntimeParameter) + } + + return $runtimeParameterDictionary + } + + Context "Error Handling" { + + It "Writes a Warning and Returns Early if Region is Not Supplied and Execution is Not in AWS" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + + + Clear-AwsSqsQueue -ProfileName "bippity-boppity" -QueueName "AintNoWayThisIsALegitPrefix" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "No region was supplied and this command is not running in AWS" } + + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Returns Early if Profile is Not Supplied and Execution is Not in AWS" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + + Clear-AwsSqsQueue -Region "antartica-south-1" -QueueName "AintNoWayThisIsALegitPrefix" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "No profile was supplied and this command is not running in AWS" } + + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Returns Early if AllMatchedQueues Is Supplied and Execution is Not in AWS" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "bippity-boppity" -AllMatchedQueues + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "calculated queue names are only available in AWS" } + + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Returns Early if No Queues are Found with the Search Value" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { return $null } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "bippity-boppity" -QueueName "FooToDaBar" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "No matching queues found" } + + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It + } + } + + Context "Logic" { + + It "Iterates over All Queues When There are Paged Results Available" { + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + # Next Token is returned by AWS when there is another page of results + NextToken = "1"; + QueueUrls = @("FirstGetCallQueueA","FirstGetCallQueueB"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + # Next Token is returned by AWS when there is another page of results + NextToken = ""; + QueueUrls = @("SecondGetCallQueueA","SecondGetCallQueueB"); + } + } -ParameterFilter { ([string]$NextToken).Length -gt 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "TestData" + + Assert-MockCalled -CommandName Get-SQSQueue -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 4 -Exactly -Scope It + } + + It "Prompts the User to Confirm if More than 5 Queues are Targeted and Force is Not Supplied" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("1","2","3","4","5","NumberBlocks"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "AintNoWay" -Force:$false + Assert-MockCalled -CommandName Test-ShouldContinue -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 6 -Exactly -Scope It + } + + It "Does Not Prompt the User to Confirm if Less than 6 Queues are Targeted" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("6","7","8","9","10"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "TheyGonnaStopMyFlow" -Force:$false + Assert-MockCalled -CommandName Test-ShouldContinue -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 5 -Exactly -Scope It + } + + It "Writes a Warning and Exits if the User Selects No to All at the Test-ShouldContinue Prompt" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("6","7","8","9","10", "NumberBlocks"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "DontListenToTheBuzz" -Force:$false + Assert-MockCalled -CommandName Test-ShouldContinue -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "User aborted process" } + } + + It "Does Not Prompt the User if they Supply the Force Flag" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { $false } + Mock -CommandName Test-ShouldProcess -ModuleName $moduleForMock -MockWith { $false } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("6","7","8","9","10", "NumberBlocks"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "DontGetCaughtUpInTheHype" -Force + + Assert-MockCalled -CommandName Test-ShouldContinue -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Test-ShouldProcess -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Clear-SQSQueue -Times 6 -Exactly -Scope It + } + + It "Removes Dead Letter Queue Matches if Not Specified to Be Purged" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Test-ShouldProcess -ModuleName $moduleForMock -MockWith { $true } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("Foo","Foo-Dead"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "YoullLoseEverythingThatYouWorkedFor" -IncludeDeadLetterQueues:$false + Assert-MockCalled -CommandName Clear-SQSQueue -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 0 -Exactly -Scope It ` + -ParameterFilter { $QueueUrl -like "*Dead"} + } + + It "Includes Dead Letter Queue Matches if Specified to Be Purged" { + + Mock -CommandName Test-ShouldContinue -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Test-ShouldProcess -ModuleName $moduleForMock -MockWith { $true } + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("Foo","Foo-Dead"); + } + } -ParameterFilter { ([string]$NextToken).Length -eq 0 } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueuePrefix "IfItAintBrokeDontFixIt" -IncludeDeadLetterQueues + Assert-MockCalled -CommandName Clear-SQSQueue -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Clear-SQSQueue -Times 1 -Exactly -Scope It ` + -ParameterFilter { $QueueUrl -eq "Foo-Dead"} + } + } + + Context "AllMatchedQueues Parameter Set" { + + It "Constructs the Queue Prefix from Machine Config Values in the AllMatchedQueues Path" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return "FightClub" } + Mock -CommandName Get-EnvironmentNameSafeDesignation -ModuleName $moduleForMock -MockWith { return "HisNameIsRobertPaulson" } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -AllMatchedQueues + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-SQSQueue -Times 1 -Exactly -Scope It ` + -ParameterFilter { $QueueNamePrefix -Match "FightClub-HisNameIsRobertPaulson" } + } + } + + Context "QueueName Parameter Set" { + + It "Does Not Purge a Queue if a Dead Letter Queue Name is Specified without the DeadLetterQueue flag in the QueueName Path" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("TestQueue-Dead"); + } + } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueueName "TestQueue-DEAD" + + Assert-MockCalled -CommandName Clear-SQSQueue -Scope It -Times 0 -Exactly + } + + It "Clears Only Exact Matches on Queue Name in the QueueName Path" { + + Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { $true } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Mock -CommandName Get-SQSQueue -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + + NextToken = ""; + QueueUrls = @("https://MatchingQueue", "https://MatchingQueue2", "https://MatchingQueue3", "https://NonMatchingQueue4"); + } + } + + Clear-AwsSqsQueue -Region "antartica-south-1" -ProfileName "boo" -QueueName "MatchingQueue" + + Assert-MockCalled -CommandName Clear-SQSQueue -Scope It -Times 1 -Exactly -ParameterFilter { $QueueUrl -eq "https://MatchingQueue" } + Assert-MockCalled -CommandName Clear-SQSQueue -Scope It -Times 0 -Exactly -ParameterFilter { $QueueUrl -match "MatchingQueue\d" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.ps1 new file mode 100644 index 0000000..c155e5f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.ps1 @@ -0,0 +1,116 @@ +function Clear-CloudFlareCache { + <# +.SYNOPSIS + Clears CloudFlare Cache for a Given Zone and Hosts + +.DESCRIPTION + Makes an API call to clear CloudFlare cache for a particular zone, and limits the request by Host. This has to be + limited due to our OLBEDGE.NET zone which houses multiple FIs that also leverage CloudFlare. Host counts over 30 will + be split in to multiple API calls due to API filter limits + +.PARAMETER AuthenticationDictionary + Authentication headers required to execute requests. This object is the result from + calls to Get-CloudFlareAuthenticationHeaders + +.PARAMETER ZoneId + [string] The parent zone ID for the host + +.PARAMETER HostNames + [string[]] An array of hostnames used to filter the cache bust + +.PARAMETER BaseUri + [string] The base CloudFlare API URL. Defaults to https://api.cloudflare.com/client/v4 + +.PARAMETER MaxHosts + [int] The maximum number of hosts the API supports. Defaults to 30. When the hosts provided exceeds this count, multiple API calls will be made + +.LINK + +https://api.cloudflare.com/#zone-purge-files-by-cache-tags-or-host +#> + + [CmdletBinding()] + param( + + [Parameter(Mandatory = $true)] + [Alias("CloudFlareAuthenticationHeaders")] + [System.Collections.Generic.Dictionary[[String],[String]]]$AuthenticationDictionary, + + [Parameter(Mandatory = $true)] + [Alias("CloudFlareZoneId")] + [string[]]$ZoneId, + + [Parameter(Mandatory = $true)] + [Alias("Host")] + [string[]]$HostNames, + + [Parameter(Mandatory = $false)] + [Alias("CloudFlareBaseUri")] + [string]$BaseUri = "https://api.cloudflare.com/client/v4", + + [Parameter(Mandatory = $false)] + [Alias("MaxHostsCount")] + [int]$MaxHosts = 30, + + [Parameter(Mandatory = $false)] + [bool]$WriteCacheClearErrorsAsErrors = $true + ) + $logLead = Get-LogLeadName + $cacheClearURL = "$BaseUri/zones/$ZoneId/purge_cache" + if (!(Test-IsCollectionNullOrEmpty -Collection $HostNames) -and $HostNames.Count -gt 1) { + $hostDescriptor = "Hosts" + } else { + $hostDescriptor = "Host" + } + + # Group Hosts In to Sets Based on $MaxHosts + $hostCounter = New-Object PSObject -Property @{ HostCount = 0; } + $hostGroups = $HostNames | Group-Object -Property { [Math]::Floor($hostCounter.HostCount++ / $MaxHosts) } + + # Because of the way groups work, the Count property is the count of groups if more than 1, or the count of group + # objects if exactly 1. It is less than ideal + if ($null -ne $hostGroups[1]) { + $hostGroupsCount = $hostGroups.Count + Write-Warning "$logLead : Found $hostGroupsCount Host Groups Based on API Limit. Multiple API Calls Will Be Made" + + # if someone can explain the point of this loop to me, that would be great + # trowton - 2022-02-14 + $i = 1 + foreach ($hostGroup in $hostGroups) { + Write-Verbose "$logLead : [Group $i of $hostGroupsCount] : $($hostGroup.Group)" + $i++ + } + } + + # Iterate Over Each Group + foreach ($hostGroup in $hostGroups) { + + $hostGroupTargets = $hostGroup.Group + $data = (@{ hosts = @( $hostGroupTargets ) } | ConvertTo-Json) + + Write-Host "$logLead : Calling $cacheClearURL to Clear Cache for $hostDescriptor [$hostGroupTargets]" + Write-Verbose "$logLead : Request Body: $data" + + $cacheClearResult = Invoke-RestMethod -Method Post -ContentType "application/json" -Header $AuthenticationDictionary ` + -Uri "$cacheClearURL" -Body $data + + Write-Verbose ("$logLead : CloudFlare Response: {0}" -f ($cacheClearResult | ConvertTo-Json -Depth 3) ) + + if (!($cacheClearResult.Success)) { + Write-Warning "$logLead : An Error Occurred Making the API Call:" + if ($WriteCacheClearErrorsAsErrors -eq $true) { + Write-Warning "Detected this is a production job run and will print errors as errors!" + if ($null -ne $cacheClearResult.errors) { + $cacheClearResult.errors | Select-Object -ExpandProperty Message | ForEach-Object { Write-Host "##teamcity[buildProblem description='$_']" } + } + } else { + Write-Host "Detected this is a non-production job run and will print errors as warnings!" + if ($null -ne $cacheClearResult.errors) { + $cacheClearResult.errors | Select-Object -ExpandProperty Message | ForEach-Object { Write-Host "##teamcity[message text='$_' status='WARNING']" } + } + } + } else { + Write-Host "$logLead : Successfully Cleared Cache for $hostDescriptor [$hostGroupTargets]" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.tests.ps1 new file mode 100644 index 0000000..e897ff3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCache.tests.ps1 @@ -0,0 +1,148 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Clear-CloudFlareCache" { + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + $fakeAuthCredentials = Get-CloudFlareAuthenticationHeaders "fake.mcfake@email.org" "123456" + $fakeZoneID = "123456" + $fakeHostName1 = "fakehost" + $fakeHostName2 = "fakehost2" + $fake35HostArray = @( "firsttechqastg.orb.alkamitech.com", "admin-firsttechqastg.orb.alkamitech.com", "firsttechqastg-ip.orb.alkamitech.com", + "fsbarcadiaqastg.orb.alkamitech.com", "admin-fsbarcadiaqastg.orb.alkamitech.com", "fsbarcadiaqastg-ip.orb.alkamitech.com", + "charlotteqastg.orb.alkamitech.com", "admin-charlotteqastg.orb.alkamitech.com", "charlotteqastg-ip.orb.alkamitech.com", + "educatorsqastg.orb.alkamitech.com", "admin-educatorsqastg.orb.alkamitech.com", "educatorsqastg-ip.orb.alkamitech.com", + "empowerqastg.orb.alkamitech.com", "admin-empowerqastg.orb.alkamitech.com", "empowerqastg-ip.orb.alkamitech.com", + "iccuqastg.orb.alkamitech.com", "admin-iccuqastg.orb.alkamitech.com", "iccuqastg-ip.orb.alkamitech.com", "inspirusqastg.orb.alkamitech.com", + "admin-inspirusqastg.orb.alkamitech.com", "inspirusqastg-ip.orb.alkamitech.com", "kernschoolsqastg.orb.alkamitech.com", + "admin-kernschoolsqastg.orb.alkamitech.com", "kernschoolsqastg-ip.orb.alkamitech.com", "lgeqastg.orb.alkamitech.com", + "admin-lgeqastg.orb.alkamitech.com", "lgeqastg-ip.orb.alkamitech.com", "missionqastg.orb.alkamitech.com", "admin-missionqastg.orb.alkamitech.com", + "missionqastg-ip.orb.alkamitech.com", "patelcoqastg.orb.alkamitech.com", "admin-patelcoqastg.orb.alkamitech.com", "patelcoqastg-ip.orb.alkamitech.com", + "wauchulaqastg.orb.alkamitech.com", "admin-wauchulaqastg.orb.alkamitech.com" ) + + $fakeSingleHostBody = (@{ hosts = @( $fakeHostName1 ) } | ConvertTo-Json) + $fakeMultiHostBody = (@{ hosts = @( $fakeHostName1, $fakeHostName2 ) } | ConvertTo-Json) + $fake35HostBody = (@{ hosts = @( $fake35HostArray ) } | ConvertTo-Json) + $fake35HostBodyGroup1 = (@{ hosts = @( ($fake35HostArray | Select-Object -First 30) ) } | ConvertTo-Json) + $fake35HostBodyGroup2 = (@{ hosts = @( $fake35HostArray | Select-Object -Skip 30 ) } | ConvertTo-Json) + + Context "Parameter Validation" { + Mock -CommandName Invoke-RestMethod -MockWith { + + [PSCustomObject]@{ + Uri = $uri; + Method = $method; + ContentType = $contentType; + Header = $header; + Body = $body; + Success = $true; + } + + } -ModuleName $moduleForMock + + $fakeZoneDefaultUrl = "https://api.cloudflare.com/client/v4/zones/$fakeZoneID/purge_cache" + + It "Defaults to CloudFlare API v4" { + + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID $fakeHostName1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $uri -like "*$fakeZoneDefaultUrl*" } + } + + It "Uses the Parameterized URL in the Rest Call" { + + $fakeZoneFakeUrl = "https://fake.api.com/client/v9000/zones/$fakeZoneID/purge_cache" + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID $fakeHostName1 $fakeZoneFakeUrl + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $uri -like "*$fakeZoneFakeUrl*" } + } + + It "Uses a Single HostName in the Body if Specified" { + + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID $fakeHostName1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $body -eq $fakeSingleHostBody } + } + + It "Uses an Array of Hosts in the Body if Specified" { + + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID @($fakeHostName1, $fakeHostName2) + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $body -eq $fakeMultiHostBody } + } + + It "Splits Calls When More than 30 Hosts Provided By Default" { + + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID $fake35HostArray + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $body -eq $fake35HostBodyGroup1 } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $body -eq $fake35HostBodyGroup2 } + } + + It "Uses the MaxHosts Parameter to Determine Host Split Count" { + + Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID $fake35HostArray -MaxHosts 35 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $body -eq $fake35HostBody } + } + } + + Context "Response Evaluation" { + + It "Continues Without Error on Success" { + + Mock -CommandName Invoke-RestMethod -MockWith { + + [PSCustomObject]@{ + Uri = $uri; + Method = $method; + ContentType = $contentType; + Header = $header; + Body = $body; + Success = $true; + } + + } -ModuleName $moduleForMock + + { Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID @($fakeHostName1, $fakeHostName2) } ` + | Should -Not -Throw + } + + It "Continues Without Throwing on CF Error with Warning" { + + Mock -CommandName Write-Warning -MockWith { + + [PSCustomObject]@{ + Message = $Message + } + + } -ModuleName $moduleForMock + + Mock -CommandName Invoke-RestMethod -MockWith { + + [PSCustomObject]@{ + Uri = $uri; + Method = $method; + ContentType = $contentType; + Header = $header; + Body = $body; + Success = $false; + } + + } -ModuleName $moduleForMock + + { Clear-CloudFlareCache $fakeAuthCredentials $fakeZoneID @($fakeHostName1, $fakeHostName2) } | ` + Should -Not -Throw + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 1 ` + -ParameterFilter { $message -like "*An Error Occurred Making the API Call*" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.ps1 new file mode 100644 index 0000000..3c0371e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.ps1 @@ -0,0 +1,173 @@ +function Clear-CloudFlareCacheForSites { + + <# + +.SYNOPSIS + Clears CloudFlare Cache for All Configured Sites in IIS + +.DESCRIPTION + Describes all HTTPS bindings for admin, client, and IP-STS sites in IIS, and calls Clear-CloudFlareCache + for each. If the zone cannot be located, attempts to clear the cache for the host in the Fallback Zone + +.PARAMETER authenticationDictionary + [dictionary] Authentication headers required to execute requests. This object is the result from + calls to Get-CloudFlareAuthenticationHeaders + +.PARAMETER zoneId + [string] The parent zone ID for the host + +.PARAMETER hostFilterArray + [string[]] An optional array of hostnames used to filter the cache bust + +.PARAMETER baseUri + [string] The base CloudFlare API URL. Defaults to https://api.cloudflare.com/client/v4 + +.PARAMETER fallBackZoneName + [string] The zone to query if the zone cant be located by client URL. Defaults to olbedge.net +#> + + [CmdletBinding()] + param( + + [Parameter(Mandatory = $true)] + [Alias("CloudFlareAuthenticationHeaders")] + [System.Collections.Generic.Dictionary[[String], [String]]]$authenticationDictionary, + + [Parameter(Mandatory = $false)] + [Alias("HostFilter")] + [string[]]$hostFilterArray, + + [Parameter(Mandatory = $false)] + [Alias("CloudFlareBaseUri")] + [string]$baseUri = "https://api.cloudflare.com/client/v4", + + [Parameter(Mandatory = $false)] + [Alias("FallBackZone")] + [string]$fallBackZoneName = "olbedge.net", + + [Parameter(Mandatory = $false)] + [Alias("ReturnActiveOnly")] + [bool]$ActiveOnly, + + [Parameter(Mandatory = $false)] + [bool]$WriteCacheClearErrorsAsErrors = $true + + ) + + $logLead = Get-LogLeadName + + Write-Verbose "$logLead : Pulling IIS Sites" + $sites = Get-IISSiteList -IncludeIPSTS + + $httpsBindings = $sites.Bindings | Where-Object { $_.Protocol -like "https" } + + [array]$hostsToClear = @() + + if (Test-IsCollectionNullOrEmpty $hostFilterArray) { + + # No filter passed in, we're gonna clear 'em all + $hostsToClear = $httpsBindings | Select-Object -ExpandProperty Host + + } else { + + foreach ($hostFilter in $hostFilterArray) { + + # Find Matching Sites to Clear + $matchingSites = $httpsBindings | Where-Object { $_.Host -eq $hostFilter } + + if ($null -eq $matchingSites) { + + # Couldn't find a match from IIS vs. the filter + Write-Warning "$logLead : Unable to find matching site for supplied filter value $hostFilter" + + } else { + + $hostsToClear += ($matchingSites | Select-Object -ExpandProperty Host) + } + } + } + + $knownZones = @() + $targetsToClear = @() + + foreach ($targetHost in $hostsToClear) { + + # Split on '.' and take the last two values to calculate the zone + $zoneName = [String]::Join(".", ($targetHost -split "\." | Select-Object -Last 2)) + $knownZone = $knownZones | Where-Object { $_.ZoneName -eq $zoneName } | Select-Object -First 1 + + # We haven't already looked up this zone, so we need to get the Zone ID + if ($null -eq $knownZone) { + + $zoneId = Get-CloudFlareZoneId -AuthenticationHeaders $authenticationDictionary -ZoneName $zoneName -CloudFlareBaseUri $baseUri -ActiveOnly $ActiveOnly + + if ($null -eq $zoneId) { + + # No zone ID, no cache clearing. + Write-Warning "$logLead : Could Not Pull Zone ID for Host $targetHost." + + if ($null -eq $fallBackZoneName) { + + # Moving on + Write-Warning "$logLead : No Fallback Zone Name was Supplied -- Cache Will Not Be Cleared" + continue; + } + + Write-Host "$logLead : Will attempt to clear site cache using fallback zone $fallBackZoneName" + + $fallBackZoneId = ($knownZones | Where-Object { $_.ZoneName -eq $fallBackZoneName } | Select-Object -First 1).ZoneId + if ($null -eq $fallBackZoneId) { + + Write-Host "$logLead : Looking up zone ID for fallback zone $fallBackZoneName" + $zoneId = Get-CloudFlareZoneId -AuthenticationHeaders $authenticationDictionary -ZoneName $fallBackZoneName -CloudFlareBaseUri $baseUri -ActiveOnly $ActiveOnly + $knownZones += @{ZoneName = $fallBackZoneName; ZoneId = $zoneId } + } + + if ($null -eq $zoneId) { + + if ($null -eq $fallBackZoneId) { + + # We can't find the primary or fallback zone ID. Moving on + Write-Warning "$logLead : Could not find Zone or Fallback Zone ID. Execution for host $targetHost cannot continue" + continue; + + } else { + + # Bust with Fallback Zone ID -- they're either in olbedge or they don't exist + Write-Host "$logLead : Using Previously Retrieved Fallback Zone ID $fallBackZoneID for host $targetHost" + $zoneId = $fallBackZoneId + } + } + } + + Write-Verbose "$logLead : Adding Zone $zoneName to known zones with ID $zoneId" + $knownZones += @{ZoneName = $zoneName; ZoneId = $zoneId } + + Write-Verbose "$logLead : Adding Cache Clear Target $targetHost with Zone ID $zoneId" + $targetsToClear += ( New-Object PSObject -Property @{ HostName = $targetHost; ZoneId = $zoneId; } ) + + } else { + + Write-Verbose "$logLead : Using Known Zone ID $($knownZone.ZoneId)" + + Write-Verbose "$logLead : Adding Cache Clear Target $targetHost with Zone ID $($knownZone.ZoneId)" + $targetsToClear += ( New-Object PSObject -Property @{ HostName = $targetHost; ZoneId = $knownZone.ZoneId; } ) + } + } + + if (Test-IsCollectionNullOrEmpty $targetsToClear) { + + Write-Warning "$logLead : No valid targets to clear. See the prior log output for details" + return; + } + + $groupedTargets = ($targetsToClear | Group-Object -Property ZoneId -AsString -AsHashTable) + + foreach ($target in $groupedTargets.GetEnumerator()) { + + $hostNamesForZone = ($target.Value | Select-Object -ExpandProperty Hostname) + $zoneIdToBust = $target.Name + Write-Verbose ("$logLead : Clearing Cache for Zone ID $zoneIdToBust. Target sites: [{0}]" -f ($hostNamesForZone -join "," )) + Clear-CloudFlareCache -CloudFlareAuthenticationHeaders $authenticationDictionary -CloudFlareZoneId $zoneIdToBust -Host $hostNamesForZone -CloudFlareBaseUri $baseUri -WriteCacheClearErrorsAsErrors $WriteCacheClearErrorsAsErrors + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.tests.ps1 new file mode 100644 index 0000000..b64601a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-CloudFlareCacheForSites.tests.ps1 @@ -0,0 +1,266 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +#Write-Host "Overriding SUT: $functionPath" +#Import-Module $functionPath -Force +$moduleForMock = "" +$moduleForMock = "Alkami.DevOps.Operations" + +Describe "Clear-CloudFlareCacheForSites" { + + $fakeAuthCredentials = Get-CloudFlareAuthenticationHeaders "fake.mcfake@email.org" "123456" + $fakeZoneID = "123456" + + Mock -CommandName Invoke-RestMethod -MockWith { + + [PSCustomObject]@{ + Uri = $uri; + Method = $method; + ContentType = $contentType; + Header = $header; + Body = $body; + Success = $true; + } + + } -ModuleName $moduleForMock + + Mock -CommandName Get-IISSiteList -MockWith { + + $adminBindings = @( + + (New-Object PSObject -Property @{ + + host = "admin.fakesite.com" + protocol = "http" + }), + + (New-Object PSObject -Property @{ + + host = "admin.fakesite.com" + protocol = "https" + }) + ) + + $clientBindings = @( + + (New-Object PSObject -Property @{ + + host = "client.fakesite.com" + protocol = "http" + }), + + (New-Object PSObject -Property @{ + + host = "client.fakesite.com" + protocol = "https" + }) + ) + + $ipstsBindings = @( + + (New-Object PSObject -Property @{ + + host = "ip.fakesite.com" + protocol = "http" + }), + + (New-Object PSObject -Property @{ + + host = "ip.fakesite.com" + protocol = "https" + }) + ) + + $sites = @( + [PsCustomObject]@{ + Bindings = $adminBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $clientBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $ipstsBindings + State = "Started" + } + ) + + return $sites + + } -ModuleName $moduleForMock + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + Mock -CommandName Get-CloudFlareZoneId -MockWith { "123456" } -ModuleName $moduleForMock + + It "Defaults to CloudFlare API v4" { + + Clear-CloudFlareCacheForSites $fakeAuthCredentials + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $uri -like "*$fakeZoneDefaultUrl*" } + } + + It "Uses the Parameterized URL in the Rest Call" { + + $fakeZoneFakeUrl = "https://fake.api.com/client/v9000/zones/$fakeZoneID/purge_cache" + Clear-CloudFlareCacheForSites $fakeAuthCredentials -baseUri $fakeZoneFakeUrl + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $uri -like "*$fakeZoneFakeUrl*" } + } + + It "Defaults to Fallback Zone OLBEdge.Net" { + + Mock -CommandName Get-CloudFlareZoneId -MockWith { $null } -ParameterFilter { $ZoneName -ne "olbedge.net" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { "123" } -ParameterFilter { $ZoneName -eq "olbedge.net" } -ModuleName $moduleForMock + + Clear-CloudFlareCacheForSites $fakeAuthCredentials + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -like "*olbedge.net*" } + } + + It "Uses the Parameterized FallBackZoneName in the Rest Call" { + + Mock -CommandName Get-CloudFlareZoneId -MockWith { $null } -ParameterFilter { $ZoneName -ne "fakefallback.net" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { "678" } -ParameterFilter { $ZoneName -eq "fakefallback.net" } -ModuleName $moduleForMock + + Clear-CloudFlareCacheForSites $fakeAuthCredentials -FallBackZone "fakefallback.net" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -like "*fakefallback.net*" } + } + } + + Context "Zone Lookup" { + + It "Does Not Lookup the Fallback Zone if the Primary Is Found" { + + Mock -CommandName Get-CloudFlareZoneId -MockWith { "999" } -ParameterFilter { $ZoneName -ne "olbedge.net" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { $null } -ParameterFilter { $ZoneName -eq "olbedge.net" } -ModuleName $moduleForMock + + Clear-CloudFlareCacheForSites $fakeAuthCredentials + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -notlike "*olbedge.net*" } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 0 ` + -ParameterFilter { $ZoneName -like "*olbedge.net*" } + } + + It "Uses the Fallback Zone if the Primary Is not Found" { + + Mock -CommandName Get-CloudFlareZoneId -MockWith { $null } -ParameterFilter { $ZoneName -ne "olbedge.net" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { "999" } -ParameterFilter { $ZoneName -eq "olbedge.net" } -ModuleName $moduleForMock + + Clear-CloudFlareCacheForSites $fakeAuthCredentials | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -notlike "*olbedge.net*" } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -like "*olbedge.net*" } + } + + It "Only Looks Up Unique Zones Once" { + + Mock -CommandName Get-IISSiteList -MockWith { + + $bindings = @( + + (New-Object PSObject -Property @{ + + host = "admin.fakesite.com" + protocol = "https" + }), + + (New-Object PSObject -Property @{ + + host = "admin2.fakesite.com" + protocol = "https" + }) + + (New-Object PSObject -Property @{ + + host = "admin.fakesite2.com" + protocol = "https" + }) + ) + + $sites = @( + [PsCustomObject]@{ + Bindings = $bindings + State = "Started" + } + ) + + return $sites + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CloudFlareZoneId -MockWith { return "123456" } -ParameterFilter { $ZoneName -like "*fakesite.com*" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { return "789101" } -ParameterFilter { $ZoneName -like "*fakesite2.com*" } -ModuleName $moduleForMock + + Clear-CloudFlareCacheForSites $fakeAuthCredentials + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -like "*fakesite.com*" } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CloudFlareZoneId -Scope It -Exactly 1 ` + -ParameterFilter { $ZoneName -like "*fakesite2.com*" } + } + } + + Context "Zone Grouping" { + + It "Groups Items By Zone ID" { + + Mock -CommandName Get-IISSiteList -MockWith { + + $bindings = @( + + (New-Object PSObject -Property @{ + + host = "admin.fakesite.com" + protocol = "https" + }), + + (New-Object PSObject -Property @{ + + host = "admin.fakesite2.com" + protocol = "https" + }), + + (New-Object PSObject -Property @{ + + host = "admin2.fakesite.com" + protocol = "https" + }) + ) + + $sites = @( + [PsCustomObject]@{ + Bindings = $bindings + State = "Started" + } + ) + + return $sites + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CloudFlareZoneId -MockWith { return "123456" } -ParameterFilter { $ZoneName -like "*fakesite.com*" } -ModuleName $moduleForMock + Mock -CommandName Get-CloudFlareZoneId -MockWith { return "789101" } -ParameterFilter { $ZoneName -like "*fakesite2.com*" } -ModuleName $moduleForMock + + $fakeMultiHostBody = (@{ hosts = @( "admin.fakesite.com", "admin2.fakesite.com" ) } | ConvertTo-Json) + + Clear-CloudFlareCacheForSites $fakeAuthCredentials + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $Body -eq $fakeMultiHostBody } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-RestMethod -Scope It -Exactly 1 ` + -ParameterFilter { $Body -ne $fakeMultiHostBody } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.ps1 new file mode 100644 index 0000000..9bd593f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.ps1 @@ -0,0 +1,135 @@ +function Clear-RadiumJobs { + <# + .SYNOPSIS + Executes a T-SQL script to purge the Radium queue + + .DESCRIPTION + Executes to the master database and executes an operational script which sets running jobs + to a cancelled state using a variety of criteria. + + .PARAMETER MasterConnectionString + The master database connection string. If not supplied is attempted to be read from the machine.config + + .PARAMETER SecondsBetweenExecution + The number of seconds to pause between executions of the purge script. Defaults to 15 and must be no less than 5 + + .PARAMETER QueueCountFloor + The minimum number of matching jobs which are in running or waiting status before a purge will execute. Defaults to 500 + + .PARAMETER MinimumJobAgeSeconds + The minimum age in seconds of a job before it will be considered for purging. Defaults to 15 + + .PARAMETER MaximumIterations + The maximum number of iterations to execute the purge job. Defaults to 7,200, or 2 hours at the default frequency, with the expectation that someone will cancel execution once the incident is resolved. + + .PARAMETER JobFilter + When supplied, will filter eligible jobs for purging by the job type. Valid values are 'SyncAccountAndTransactionsJob', 'SyncBillPayJob', 'SyncUserJob', or 'SyncTransactionsJob' + + .LINK + Runbook: https://confluence.alkami.com/x/rbBiDg + + .EXAMPLE + Clear-RadiumJobs -SecondsBetweenExecution 5 -QueueCountFloor 50 + + [Clear-RadiumJobs] : Beginning Radium purge. This command will run until cancelled. To cancel, press CONTROL+C or close the shell. + 12-21-2022 11:29:55 PM : Queue size (0) under threshold for purge (50). No action taken. + + .EXAMPLE + Clear-RadiumJobs -SecondsBetweenExecution 5 -QueueCountFloor 50 -JobFilter SyncBillPayJob + + [Clear-RadiumJobs] : Beginning Radium purge. This command will run until cancelled. To cancel, press CONTROL+C or close the shell. + 12-21-2022 11:29:55 PM : Queue size (0) for SyncBillPayJob jobs under threshold for purge (50). No action taken. + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$MasterConnectionString = $null, + + [Parameter(Mandatory = $false)] + [int]$SecondsBetweenExecution = 15, + + [Parameter(Mandatory = $false)] + [int]$QueueCountFloor = 500, + + [Parameter(Mandatory = $false)] + [int]$MinimumJobAgeSeconds = 15, + + [Parameter(Mandatory = $false)] + [int]$MaximumIterations = 7200, + + [Parameter(Mandatory = $false)] + [ValidateSet("SyncAccountAndTransactionsJob", "SyncBillPayJob", "SyncUserJob", "SyncTransactionsJob")] + [string]$JobFilter = $null + ) + + $logLead = Get-LogLeadName + $useJobFilterScriptVariant = !(Test-StringIsNullOrWhiteSpace -Value $JobFilter) + + Write-Host ("$logLead : Beginning Radium purge. This command will run until cancelled or until $MaximumIterations runs have executed. " + ` + "To cancel, press CONTROL+C or close the shell.") -ForegroundColor Yellow + + $targetMasterConnectionString = Get-CoalescedStringValue -ValueA $MasterConnectionString -ValueB (Get-MasterConnectionString) + if (Test-StringIsNullOrWhitespace -Value $targetMasterConnectionString) { + + $emptyConnectionStringWarning = "$logLead : No master connection string was supplied to the function, and the connection string was not found in the machine.config. " + ` + "Re-Execute on a properly configured server or supply an explicit connection string to the function." + Write-Warning $emptyConnectionStringWarning + + return + } + + if ($SecondsBetweenExecution -lt 5) { + + Write-Warning "$logLead : To avoid negative impact on the database server, SecondsBetweenExecution must be no less than 5. Correct the parameter and rerun." + return + } + + if ($MinimumJobAgeSeconds -eq 0) { + + Write-Warning "$logLead : MinimumJobAgeSeconds must not be equal to 0. Correct the parameter and rerun." + return + + } elseif ($MinimumJobAgeSeconds -gt 0) { + + Write-Verbose "$logLead : Converting MinimumJobAgeSeconds value [$MinimumJobAgeSeconds] to a negative value" + $MinimumJobAgeSeconds = $MinimumJobAgeSeconds * -1 + } + + $radiumPurgeScript = Read-RadiumPurgeScript -UseJobFilterScriptVariant $useJobFilterScriptVariant + if (Test-StringIsNullOrWhitespace -Value $radiumPurgeScript) { + + Write-Warning "$logLead : Unable to read the Radium purge script. Execution cannot continue. Notify SRE for investigation" + return + } + + $parameters = @( + + @{ Name = "@QueueCountThreshold"; Value = $QueueCountFloor; }, + @{ Name = "@AgeThresholdSeconds"; Value = $MinimumJobAgeSeconds; } + ) + + if ($useJobFilterScriptVariant) { + + $parameters += @{ Name = "@JobFilter"; Value = $JobFilter } + } + + $iteration = 1 + do { + + Invoke-NonQueryByConnectionString -ConnectionString $targetMasterConnectionString -QueryString $radiumPurgeScript -SqlInputParameters $parameters -CommandTimeout 5 + + Write-Verbose "[Iteration: $iteration / $MaximumIterations]: Sleeping $SecondsBetweenExecution seconds between executions" + + if ($iteration -lt $MaximumIterations) { + + # No Need to Sleep if We're on the Last Execution + Start-Sleep -Seconds $SecondsBetweenExecution + } + + $iteration++ + + } while ($iteration -le $MaximumIterations) + + Write-Host "$logLead : Completed $($iteration - 1) Iterations of the Purge Script. Loop stopped." +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.tests.ps1 new file mode 100644 index 0000000..49471e8 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Clear-RadiumJobs.tests.ps1 @@ -0,0 +1,136 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Clear-RadiumJobs" { + + Mock -CommandName Invoke-NonQueryByConnectionString -ModuleName $moduleForMock -MockWith {} + + Context "Logic" { + + It "Uses the Value for MasterConnectionString if Supplied" { + + $fakeConnectionString = "data source=ZeroWing.local;Integrated Security=SSPI;Database=AllYourBaseAreBelongtoUs" + Clear-RadiumJobs -MasterConnectionString $fakeConnectionString -MaximumIterations 1 + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $ConnectionString -eq $fakeConnectionString } + } + + It "Uses the machine.config for the MasterConnectionString if Not Supplied" { + + Mock -CommandName Get-MasterConnectionString -ModuleName $moduleForMock -MockWith { return "data source=i.can.haz;Integrated Security=SSPI;Database=Cheezburgur"} + Clear-RadiumJobs -MaximumIterations 1 + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $ConnectionString -eq "data source=i.can.haz;Integrated Security=SSPI;Database=Cheezburgur" } + } + + It "Uses the Value for SecondsBetweenExecution if Supplied" { + + Mock -CommandName Start-Sleep -ModuleName $moduleForMock -MockWith {} + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 2 -SecondsBetweenExecution 5 + + Assert-MockCalled -CommandName Start-Sleep -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Seconds -eq 5 } + } + + It "Uses the Value for QueueCountFloor if Supplied" { + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 1 -QueueCountFloor 1024 + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { + + ($SqlInputParameters | Where-Object {$_.Name -eq "@QueueCountThreshold"}).Value -eq 1024 + } + } + + It "Uses the Value for MinimumJobAgeSeconds if Supplied" { + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 1 -MinimumJobAgeSeconds -2048 + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { + + ($SqlInputParameters | Where-Object {$_.Name -eq "@AgeThresholdSeconds"}).Value -eq -2048 + } + } + + It "Converts Positive Integers for MinimumJobAgeSeconds to Negative Values" { + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 1 -MinimumJobAgeSeconds 4096 + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { + + ($SqlInputParameters | Where-Object {$_.Name -eq "@AgeThresholdSeconds"}).Value -eq -4096 + } + } + + It "Runs Only for the Maximum Number of Iterations Supplied" { + + Mock -CommandName Start-Sleep -ModuleName $moduleForMock -MockWith {} + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 3 + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 3 -Exactly -Scope It + } + } + + Context "Error Handling" { + + It "Writes a Warning and Exits Early if MinimumJobAgeSeconds is equal to 0" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 3 -MinimumJobAgeSeconds 0 + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "MinimumJobAgeSeconds must not be equal to 0." } + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Exits Early if the Connection String Cannot be Determined" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-MasterConnectionString -ModuleName $moduleForMock -MockWith { return $null } + + Clear-RadiumJobs -MaximumIterations 3 + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "No master connection string was supplied to the function, and the connection string was not found in the machine.config." } + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Exits Early if the SecondsBetweenExecution is Less than 5" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Clear-RadiumJobs -MasterConnectionString "fake" -SecondsBetweenExecution 3 + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "To avoid negative impact on the database server, SecondsBetweenExecution must be no less than 5" } + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Exits Early if the Script File Cannot be Located" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Read-RadiumPurgeScript -ModuleName $moduleForMock -MockWith { return $null } + + Clear-RadiumJobs -MasterConnectionString "fake" -MaximumIterations 3 + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Unable to read the Radium purge script." } + + Assert-MockCalled -CommandName Invoke-NonQueryByConnectionString -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Close-TcpSocket.ps1 b/Modules/Alkami.DevOps.Operations/Public/Close-TcpSocket.ps1 new file mode 100644 index 0000000..7dfe520 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Close-TcpSocket.ps1 @@ -0,0 +1,16 @@ +function Close-TcpSocket { +<# +.SYNOPSIS + When given an object of type System.Net.Sockets.TCPClient, executes Close(). +.EXAMPLE + Close-TcpSocket -Socket $Socket +.INPUTS + Socket +#> + + [cmdletbinding()] + param( + $Socket + ) + $Socket.Close() +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Close-TcpStreamWriter.ps1 b/Modules/Alkami.DevOps.Operations/Public/Close-TcpStreamWriter.ps1 new file mode 100644 index 0000000..a55cd3e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Close-TcpStreamWriter.ps1 @@ -0,0 +1,15 @@ +function Close-TcpStreamWriter { +<# +.SYNOPSIS + When given an object of type System.IO.StreamWriter, executes Close(). +.EXAMPLE + Close-TcpStreamWriter -Writer $Writer +.INPUTS + Writer +#> + [cmdletbinding()] + param( + $Writer + ) + $Writer.Close() +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.ps1 b/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.ps1 new file mode 100644 index 0000000..c64b66b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.ps1 @@ -0,0 +1,137 @@ +function Convert-CloudFlareARecordsToCnames { +<# +.SYNOPSIS + Finds existing A records for Orb (client, admin, ipsts), deletes them, and creates new CNAME records + +.DESCRIPTION + Finds existing A records for Orb (client, admin, ipsts), deletes them, and creates new CNAME records. + +.PARAMETER baseUri + [string] The base CloudFlare API URL. Defaults to https://api.cloudflare.com/client/v4 + +.PARAMETER cloudflareAuthKey + [string] API key; necessary to authenticate with CloudFlare + +.PARAMETER cloudflareEmail + [string] Email account used to login to CloudFlare + +.PARAMETER targetAddress + [string] Target URI for the new CNAME records. Defaults to prod-paloaltopublic-443b9021358ce9f5.elb.us-east-1.amazonaws.com + +.PARAMETER zoneId + [string] The parent zone id for the host. + +.PARAMETER zoneName + [string] The parent zone name (ex: myfiname.com) for the host. Used to pull the zoneId. Either this or the Zone Id is required. + +#> + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + + [Parameter(Mandatory = $false)] + [Alias("CloudFlareBaseUri")] + [string]$baseUri = "https://api.cloudflare.com/client/v4", + + [Parameter(Mandatory = $true)] + [Alias("AuthKey")] + [string[]]$cloudflareAuthKey, + + [Parameter(Mandatory = $true)] + [Alias("Email")] + [string]$cloudflareEmail, + + [Parameter(Mandatory = $false)] + [string]$targetAddress = "prod-paloaltopublic-443b9021358ce9f5.elb.us-east-1.amazonaws.com", + + [Parameter(Mandatory = $true, + ParameterSetName = "ZoneId")] + [string]$zoneId, + + [Parameter(Mandatory = $true, + ParameterSetName = "ZoneName")] + [string]$zoneName + ) + + $logLead = Get-LogLeadName + + $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" + $headers.Add("X-Auth-Email", $cloudflareEmail) + $headers.Add("X-Auth-Key", $cloudflareAuthKey) + + if (!$zoneId) { + # Get zoneId using zone name + Write-Verbose("$logLead : Getting ZoneId.") + try { + $getZoneIdResult = Invoke-RestMethod -Method Get -ContentType "application/json" -Header $headers -Uri "$baseUri/zones?name=$zoneName" + } catch { + $err = $_.Exception; + Write-Warning ("$logLead : Exception getting zone Id." ) + return $err + } + $zoneId = $getZoneIdResult.result.id + Write-Verbose ("$logLead : $zoneId") + } + + # Get all dns records for provided zoneId + + Write-Verbose("$logLead : Getting A records.") + try { + $dnsRecords = Invoke-RestMethod -Method Get -ContentType "application/json" -Header $headers -Uri "$baseUri/zones/$zoneId/dns_records" + } catch { + Write-Warning ("$logLead : Exception occurred pulling A records. ") + return; + } + + # Get all A records from dns record list + $aRecords = $dnsRecords.result.Where{ $_.type -eq 'A' } + + $createResults = @(); + + if ($null -eq $aRecords -or $aRecords.count -eq 0 ) { + Write-Warning ("$logLead : No A records found. ") + } else { + foreach ($record in $aRecords) { + # Find each prefix (admin, ipsts, etc.) from the name field. These will be the name values for each of the CNAMEs + $recordPrefix = $record.name.Replace($record.zone_Name, ""); + $recordPrefix = $recordPrefix.Substring(0, $recordPrefix.length - 1); + Write-Verbose ("$logLead : Processing $recordPrefix") + + # Delete the A record + Write-Verbose("$logLead : Deleting A record $($record.name).") + try { + $deleteResult = Invoke-RestMethod -Method Delete -ContentType "application/json" -Header $headers "$baseUri/zones/$zoneId/dns_records/$($record.id)" + Write-Verbose ("$logLead : Delete Result: $($deleteResult.success)") + } catch { + $err = $_.Exception; + Write-Warning ("$logLead : Exception Deleting A record $($recordPrefix)") + break; + } + + # Create the CNAME record; AWS palo address is the default value. + Write-Verbose("$logLead : Creating CNAME record $($recordPrefix).") + $data = @{type = "CNAME"; name = $recordPrefix; content = "$targetAddress"; proxied = $true} | ConvertTo-JSON + + try { + $createResult = Invoke-RestMethod -Method Post -ContentType "application/json" -Header $headers "$baseUri/zones/$zoneId/dns_records/" -Body $data + Write-Verbose ("$logLead : $($createResult.result.id) was created.") + } catch { + $err = $_.Exception; + Write-Warning ("$logLead : Exception Creating CNAME for $($recordPrefix) : $($createResult.errors) `r`n`r`n`r`n ********** This CNAME record now needs to be created manually. **********") + break; + } + + # Confirm that a CNAME was created for each A record + try { + $createdSiteResult = Invoke-RestMethod -Method Get -ContentType "application/json" -Headers $headers -Uri "$baseUri/zones/$zoneId/dns_records/$($createResult.result.id)" + $createResults += $createdSiteResult.result; + } catch { + $err = $_.Exception; + Write-Warning ("$logLead : Create call succeded for site $($recordPrefix), but couldn't find site : $($createResult.errors)") + break; + } + } + + return $createResults + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.tests.ps1 new file mode 100644 index 0000000..0f59734 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Convert-CloudFlareARecordsToCnames.tests.ps1 @@ -0,0 +1,940 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Convert-CloudFlareARecordsToCnames" { + + Context "Non-Api call Tests" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "mock log lead" } + + It "Requires either ZoneId or ZoneName to be supplied" { + {Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" } ` + | should -Throw + } + } + + Context "Successful Tests" { + # Get Zone Id from Name + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + '{ + "result":[ + { + "id":"a32f0a74fe", + "type":"A", + "name":"my.multi.sub.zone.samplezone.com", + "content":"199.79.51.50", + "proxiable":true, + "proxied":true, + "ttl":1, + "locked":false, + "zone_id":"deada5dc1124", + "zone_name":"samplezone.com", + "modified_on":"2019-02-27T02:44:00.109525Z", + "created_on":"2019-02-27T02:44:00.109525Z", + "meta":{ + "auto_added":false, + "managed_by_apps":false, + "managed_by_argo_tunnel":false + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":3, + "total_count":3 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + # Delete A Records + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "result": { + "id": "372e67954025e0ba6aaa6d586b9e0b59" + }, + "success" : "true" + }' | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Delete" } + + # Create CNAME Records + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id":"51345ac1b00f", + "type":"CNAME", + "recs_added": 5, + "total_records_parsed": 5 + }, + "timing": { + "start_time": "2014-03-01T12:20:00Z", + "end_time": "2014-03-01T12:20:01Z", + "process_time": 1 + } + }' | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Post" } + + # Get created CNAME Record + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "result": + { + "id":"51345ac1b00f", + "type":"CNAME", + "name":"my.multi.sub.zone", + "content":"prod-paloaltopublic-443b9021358ce9f5.elb.us-east-1.amazonaws.com", + "proxiable":true, + "proxied":true, + "ttl":1, + "locked":false, + "zone_id":"deada5dc1124", + "zone_name":"samplezone.com", + "modified_on":"2018-11-13T18:03:21.771437Z", + "created_on":"2018-11-13T18:03:21.771437Z", + "meta":{ + "auto_added":false, + "managed_by_apps":false, + "managed_by_argo_tunnel":false + } + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records/*" -and ($Method -eq "Get") } + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "mock log lead" } + + It "Should Validate that A records are deleted" { + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" + + Assert-MockCalled -ModuleName $moduleForMock Invoke-RestMethod -ParameterFilter { $Method -eq "Delete" } + } + + It "Should Validate that CNAME records are created" { + $createdDomainResults = Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com"; + + Assert-MockCalled -ModuleName $moduleForMock Invoke-RestMethod -ParameterFilter { $Method -eq "Post" } + + $createdDomainResults.type | should -Not -BeNullOrEmpty + } + + It "Should Validate that CNAME records exist after having created them" { + $createdDomainResults = Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com"; + $createdDomainResults.type | should -Be "CNAME" + } + + It "Should handle subdomains correctly" { + $createdDomainResults = Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com"; + $createdDomainResults.name | should -Be "my.multi.sub.zone" # matches the name in the mock. Specifically, the name from the A records, without the zone on the end. + } + } + + Context "Error Tests" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "mock log lead" } + + It "Should Warn when no A records are found." { + + # Get zone id + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + '{ + "result":[ + ], + "result_info":{ + }, + "success":true, + "errors":[ + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" 3>&1 ` + | should -BeLike "* No A records found*" + } + + It "Should Warn when call to get A records returns non-200." { + + # Get zone id + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + throw( + '{ + "result":[ + ], + "result_info":{ + }, + "success":false, + "errors":[ + {"code":1002,"message":"Invalid dns record identifier"} + ], + "messages":[ + + ] + }') | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" 3>&1 ` + | should -BeLike "*Exception occurred pulling A records*" + } + + It "Should Warn when a delete fails" { + # Get zone id + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + '{ + "result":[ + { + "id":"a32f0a74fe", + "type":"A", + "name":"my.multi.sub.zone.samplezone.com", + "content":"199.79.51.50", + "proxiable":true, + "proxied":true, + "ttl":1, + "locked":false, + "zone_id":"deada5dc1124", + "zone_name":"samplezone.com", + "modified_on":"2019-02-27T02:44:00.109525Z", + "created_on":"2019-02-27T02:44:00.109525Z", + "meta":{ + "auto_added":false, + "managed_by_apps":false, + "managed_by_argo_tunnel":false + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":3, + "total_count":3 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + # Delete A Records - fails + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + throw('{ + "result": { + "id": "372e67954025e0ba6aaa6d586b9e0b59" + }, + "success" : false + }') | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Delete" } + + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" 3>&1 ` + | should -BeLike "*Exception Deleting A record*" + } + + It "Should Warn when a create fails" { + # Get zone id + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + '{ + "result":[ + { + "id":"a32f0a74fe", + "type":"A", + "name":"my.multi.sub.zone.samplezone.com", + "content":"199.79.51.50", + "proxiable":true, + "proxied":true, + "ttl":1, + "locked":false, + "zone_id":"deada5dc1124", + "zone_name":"samplezone.com", + "modified_on":"2019-02-27T02:44:00.109525Z", + "created_on":"2019-02-27T02:44:00.109525Z", + "meta":{ + "auto_added":false, + "managed_by_apps":false, + "managed_by_argo_tunnel":false + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":3, + "total_count":3 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + # Delete A Records + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "result": { + "id": "372e67954025e0ba6aaa6d586b9e0b59" + }, + "success" : "true" + }' | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Delete" } + + # Create CNAME Records - fails + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + throw('{ + "success": false, + "errors": [{ + "code":1004, + "message":"DNS Validation Error", + "error_chain":[ + { + "code":9003, + "message":"Invalid ''proxied'' value, must be a boolean" + } + ] + }], + "messages": [], + "result": { + }, + "timing": { + "start_time": "2014-03-01T12:20:00Z", + "end_time": "2014-03-01T12:20:01Z", + "process_time": 1 + } + }') | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Post" } + + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" 3>&1 ` + | should -BeLike "*Exception Creating CNAME for*" + } + + It "Should Warn when a create succeeds, but we can't get info about it" { + # Get Zone Id from Name + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample zone : https://api.cloudflare.com/zones?name=samplezone.com + '{ + "result":[ + { + "id":"deada5dc1124", + "name":"samplezone.com", + "status":"active", + "paused":false, + "type":"partial", + "development_mode":0, + "verification_key":"136684221-79922656", + "original_name_servers":[ + "ns49.domaincontrol.com", + "ns50.domaincontrol.com" + ], + "original_registrar":"godaddy.com, llc", + "original_dnshost":"godaddy", + "modified_on":"2019-02-27T02:45:51.557296Z", + "created_on":"2019-01-29T13:41:03.568147Z", + "activated_on":"2019-02-20T16:56:23.002562Z", + "meta":{ + "step":3, + "wildcard_proxiable":true, + "custom_certificate_quota":1, + "page_rule_quota":100, + "phishing_detected":false, + "multiple_railguns_allowed":false + }, + "owner":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "type":"organization", + "name":"Alkami" + }, + "account":{ + "id":"deada5dc1124add404f6a4fb0e60324f", + "name":"Alkami" + }, + "permissions":[ + "#analytics:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#legal:read", + "#organization:read", + "#subscription:read", + "#worker:edit", + "#worker:read", + "#zone:read", + "#zone_settings:read" + ], + "plan":{ + "id":"94f3b7b768b0458b56d2cac4fe5ec0f9", + "name":"Enterprise Website", + "price":0, + "currency":"USD", + "frequency":"monthly", + "is_subscribed":true, + "can_subscribe":true, + "legacy_id":"enterprise", + "legacy_discount":false, + "externally_managed":true + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":1, + "total_count":1 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { ($Uri -like "*zones?name=*") -and ($Method -eq "Get") } + + # Get A Records for Zone + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + # Sample dns records: https://api.cloudflare.com/zones/deada5dc1124/dns_records + # ^ fake zone id + '{ + "result":[ + { + "id":"a32f0a74fe", + "type":"A", + "name":"my.multi.sub.zone.samplezone.com", + "content":"199.79.51.50", + "proxiable":true, + "proxied":true, + "ttl":1, + "locked":false, + "zone_id":"deada5dc1124", + "zone_name":"samplezone.com", + "modified_on":"2019-02-27T02:44:00.109525Z", + "created_on":"2019-02-27T02:44:00.109525Z", + "meta":{ + "auto_added":false, + "managed_by_apps":false, + "managed_by_argo_tunnel":false + } + } + ], + "result_info":{ + "page":1, + "per_page":20, + "total_pages":1, + "count":3, + "total_count":3 + }, + "success":true, + "errors":[ + + ], + "messages":[ + + ] + }' | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records" -and ($Method -eq "Get") } + + # Delete A Records + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "result": { + "id": "372e67954025e0ba6aaa6d586b9e0b59" + }, + "success" : "true" + }' | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Delete" } + + # Create CNAME Records + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + '{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id":"51345ac1b00f", + "type":"CNAME", + "recs_added": 5, + "total_records_parsed": 5 + }, + "timing": { + "start_time": "2014-03-01T12:20:00Z", + "end_time": "2014-03-01T12:20:01Z", + "process_time": 1 + } + }' | ConvertFrom-Json + } -ParameterFilter { $Method -eq "Post" } + + # Get created CNAME Record - fails; error doesn't matter, just need a failure. + Mock -ModuleName $moduleForMock -CommandName Invoke-RestMethod -MockWith { + throw('{ + "result": + { + }, + "success":false, + "errors":[ + {"code":1002,"message":"Invalid dns record identifier"} + ], + "messages":[ + + ] + }') | ConvertFrom-Json + } -ParameterFilter { $Uri -like "*zones/*/dns_records/*" -and ($Method -eq "Get") } + + Convert-CloudFlareARecordsToCnames -cloudflareEmail "fake@alkamitech.com" -cloudflareAuthKey "FakeAuthKey" -ZoneName "FakeZone.com" 3>&1 ` + | should -BeLike "*Create call succeded for site*but couldn't find site*" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Convert-GMSAAccounts.ps1 b/Modules/Alkami.DevOps.Operations/Public/Convert-GMSAAccounts.ps1 new file mode 100644 index 0000000..c8d7cf2 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Convert-GMSAAccounts.ps1 @@ -0,0 +1,44 @@ +Function Convert-GMSAaccounts { + <# +.SYNOPSIS + Update service GMSA Service User from one that starts with the old prefix to one that starts with the new prefix + +.PARAMETER GmsaPrefixOld + Old or previous GMSA Username prefix + +.PARAMETER GmsaPrefixNew + New or current GMSA Username prefix +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$GmsaPrefixOld, + + [Parameter(Mandatory = $true)] + [string]$GmsaPrefixNew + ) + + $logLead = Get-LogLeadName + $services = Get-ChocolateyServices + + Foreach ($service in $services) { + $serviceName = $service.Name + $userName = Get-WindowsServiceUser -ServiceName $serviceName + $command = 'sc.exe' + + if ($username -like "fh\$gmsaPrefixOld.micro`$") { + $username = "fh\$gmsaPrefixNew.micro`$" + + } elseif ($username -like "fh\$gmsaPrefixOld.dbms`$") { + $username = "fh\$gmsaPrefixNew.dbms`$" + + } else { + Write-Warning -Message "$logLead : $serviceName GMSA not changed from $userName" + + } + $params = 'config', $serviceName, 'obj=', $userName, 'password= ', '' + + & $command $params + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.ps1 b/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.ps1 new file mode 100644 index 0000000..0b1fb1d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.ps1 @@ -0,0 +1,67 @@ +function ConvertTo-NormalizedLoadBalancerState { + <# + .SYNOPSIS + Takes strings from NGINX Upstream and AWS LB states and returns normalized strings for the states + .DESCRIPTION + Takes the string 'Down' from NGINX and returns 'Standby' to normalize to the AWS LB string for Standby state + .PARAMETER LBTYPE + Which LoadBalancer Type is the the InputState from? + Accepts "NGINX" or "AWS" + .PARAMETER InputState + Accepts the raw state string for NGINX or AWS LBs + .PARAMETER TreatNGINXUnhealthyAsUnhealthy + Should the NGINX 'UNhealthy' state be treated as being 'Up' which is normalized to 'Active' or returned as 'Unhealthy'? + .EXAMPLE + ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "Pending" + 'ActivePending' + ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Up" + 'Active' + ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Unhealthy" + 'Active' + ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Unhealthy" -TreatNGINXUnhealthyAsUnhealthy + 'Unhealthy' + #> + + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [ValidateSet("NGINX", "AWS")] + [string]$LBType, + [Parameter(Mandatory = $false)] + [string]$InputState, + [Parameter(Mandatory = $false)] + [switch]$TreatNGINXUnhealthyAsUnhealthy = $false + ) + + $logLead = Get-LogLeadName + + if ($LBType -eq "NGINX") { + # Handle nginx load balancers. + Write-Host "$logLead Nginx state is: [$InputState]; translating for programatic use." + if ($InputState -eq 'Up') { + return "Active" + } elseif ($InputState -eq 'Unhealthy' -and $TreatNGINXUnhealthyAsUnhealthy -eq $true) { + return "Unhealthy" + } elseif ($InputState -eq 'Unhealthy' -and $TreatNGINXUnhealthyAsUnhealthy -eq $false) { + return "Active" + } elseif ($InputState -eq 'Down') { + return "Standby" + } else { + Throw "$loglead : Unknown Nginx state: [$inputState]" + } + } elseif ($LBType -eq "AWS") { + # Handle AWS load balancers. + Write-Host "$logLead AWS State is: [$inputState]; Transforming string." + $fixedState = switch ( $inputState ) { + Pending { 'ActivePending' } + InService { 'Active' } + EnteringStandby { 'StandbyPending' } + Standby { 'Standby' } + default { Throw "$loglead : Unknown AWS LB state: [$inputState]" } + } + return $fixedState + } else { + Throw "$loglead : Unable to determine LbType: [$LBType]" + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.tests.ps1 new file mode 100644 index 0000000..1f2b1c7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/ConvertTo-NormalizedLoadBalancerState.tests.ps1 @@ -0,0 +1,76 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "ConvertTo-NormalizedLoadBalancerState" { + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Context "NGINX" { + It "up returns Active" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Up" + $result | Should -Be "Active" + } + It "UnHealthy returns Active" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Unhealthy" + $result | Should -Be "Active" + } + It "Unhealthy returns Standby" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Unhealthy" -TreatNGINXUnhealthyAsUnhealthy + $result | Should -Be "Unhealthy" + } + It "Down returns Standby" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "Down" + $result | Should -Be "Standby" + } + It "garbage in garbage out" { + try { + ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState "gbarbage" -ErrorAction Stop + } catch { + # I did this because i was getting runtimeExceptions that were not being caught by "Should -Throw" -George + $errorThrown = $true + } + $errorThrown | Should Be $true + } + } + Context "AWS" { + It "Pending returns ActivePending" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "Pending" + $result | Should -Be "ActivePending" + } + It "InService returns Active" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "InService" + $result | Should -Be "Active" + } + It "EnteringStandby returns StandbyPending" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "EnteringStandby" + $result | Should -Be "StandbyPending" + } + It "Standby returns Standby" { + $result = ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "Standby" + $result | Should -Be "Standby" + } + It "garbage in garbage out" { + try { + ConvertTo-NormalizedLoadBalancerState -LBType "AWS" -InputState "gbarbage" -ErrorAction Stop + } catch { + # I did this because i was getting runtimeExceptions that were not being caught by "Should -Throw" -George + $errorThrown = $true + } + $errorThrown | Should Be $true + } + } + Context "bad lb type input" { + It "Garbage in, throw out"{ + try { + ConvertTo-NormalizedLoadBalancerState -LBType "blarg" -InputState "gbarbage" -ErrorAction Stop + } catch { + # I did this because i was getting runtimeExceptions that were not being caught by "Should -Throw" -George + $errorThrown = $true + } + $errorThrown | Should Be $true + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.Tests.ps1 new file mode 100644 index 0000000..205c1b8 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.Tests.ps1 @@ -0,0 +1,122 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +<# +Notes: +* Did not test "Radium 2" existence +#> +Describe "Copy-AlkamiRelease" { + ## https://pester.dev/docs/usage/testdrive/ + $existingOrbPath = (Join-Path $TestDrive orb) + $tempOrbDeployPath = (Join-Path (Join-Path (Join-Path $TestDrive temp) deploy) orb) + + Context "Neither folder already exists workflow test" { + It "this should throw an error" { + { Copy-AlkamiRelease -orbDirectory $existingOrbPath -tempDirectory $tempOrbDeployPath } | Should -Throw "not found" + } + } + + $garbageFileToBeGone = (Join-Path $existingOrbPath "letmebegone.txt") + $log4netFileToBeRemain = (Join-Path $existingOrbPath "log4net.config") + $garbageFileToBeFound = (Join-Path $existingOrbPath "showme.txt") + $garbageFileToBeCopied = (Join-Path $tempOrbDeployPath "showme.txt") + + Mock -CommandName Set-AppTierFolderAndFilePermissions -MockWith { Write-Warning "I set the values for logging changes" } -ModuleName $moduleForMock + + Mock -CommandName Get-OrbPath -MockWith { return (Join-Path $TestDrive orb) } -ModuleName $moduleForMock + Mock -CommandName Get-TempOrbDeployPath -MockWith { return (Join-Path (Join-Path (Join-Path $TestDrive temp) deploy) orb) } -ModuleName $moduleForMock + + Context "Basic workflow test" { + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + New-Item -Path $garbageFileToBeCopied -ItemType File -Value "I should be in the target folder after deploy" -Force + + Copy-AlkamiRelease + + It "The file should not be there, the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + It "The file should be there, we should have copied it" { + (Test-Path $garbageFileToBeFound) | Should -Be $true + } + } + + Context "Leaves log4net.config workflow test" { + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + New-Item -Path $log4netFileToBeRemain -ItemType File -Value "I should stick around" -Force + New-Item -Path $garbageFileToBeCopied -ItemType File -Value "I should be in the target folder after deploy" -Force + + Copy-AlkamiRelease + + It "The log4net file should not be deleted" { + (Test-Path $log4netFileToBeRemain) | Should -Be $true + Write-Debug (Get-Content $log4netFileToBeRemain) + } + + It "The file should not be there, the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + It "The file should be there, we should have copied it" { + (Test-Path $garbageFileToBeFound) | Should -Be $true + } + } + + Context "When -verbose flag passed in" { + + # Stub files to make the function work + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + New-Item -Path $log4netFileToBeRemain -ItemType File -Value "I should stick around" -Force + New-Item -Path $garbageFileToBeCopied -ItemType File -Value "I should be in the target folder after deploy" -Force + + # Mocks for stuff we care about + Mock -CommandName Remove-FileSystemItem -MockWith { Write-Verbose "Called write-verbose" } -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -ModuleName $moduleForMock + + Copy-AlkamiRelease -verbose + + It "Should call Write-Verbose inside of Remove-FileSystemItem" { + Assert-MockCalled Write-Verbose -ModuleName $moduleForMock -ParameterFilter {$Message -eq "Called write-verbose"} + } + } + + Mock -CommandName Get-OrbPath -MockWith { throw "this should not get called" } -ModuleName $moduleForMock + Mock -CommandName Get-TempOrbDeployPath -MockWith { throw "this should not get called" } -ModuleName $moduleForMock + + Context "Specified folders already exist workflow test" { + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + New-Item -Path $garbageFileToBeCopied -ItemType File -Value "I should be in the target folder after deploy" -Force + + Copy-AlkamiRelease -orbDirectory $existingOrbPath -tempDirectory $tempOrbDeployPath + + It "The file should not be there, the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + It "The file should be there, we should have copied it" { + (Test-Path $garbageFileToBeFound) | Should -Be $true + } + } + + Mock -CommandName Set-AppTierFolderAndFilePermissions -MockWith { Write-Information "I set the values for logging changes" } -ModuleName $moduleForMock + + Context "Specified orb folder does not already exist workflow test" { + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeCopied -ItemType File -Value "I should be in the target folder after deploy" -Force + + Copy-AlkamiRelease -orbDirectory $existingOrbPath -tempDirectory $tempOrbDeployPath + + It "The file should be there, we should have copied it" { + (Test-Path $garbageFileToBeFound) | Should -Be $true + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.ps1 b/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.ps1 new file mode 100644 index 0000000..0d2c878 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Copy-AlkamiRelease.ps1 @@ -0,0 +1,71 @@ +function Copy-AlkamiRelease { +<# +.SYNOPSIS + Copy Orb from specified folder to destination folder. Defaults to value of Get-OrbPath + +.PARAMETER OrbDirectory + [string] This is the location where ORB is installed to. Can be left blank, will default to Get-OrbPath + +.PARAMETER TempDirectory + [string] This is where the ORB files are found. If the folder does not exist, this will throw. Defaults to Get-TempOrbDeployPath +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [Alias("OrbPath")] + [string]$OrbDirectory, + + [Parameter(Mandatory=$false)] + [Alias("TempOrbPath")] + [string]$TempDirectory + ) + + $logLead = (Get-LogLeadName) + + if([string]::IsNullOrEmpty($OrbDirectory)) { + $OrbDirectory = (Get-OrbPath) + } + if([string]::IsNullOrEmpty($TempDirectory)) { + $TempDirectory = (Get-TempOrbDeployPath) + } + + if(!(Test-Path $TempDirectory)) { + throw "$logLead : $TempDirectory folder not found - nothing to copy from" + } + + $radium2Path = (Join-Path ($OrbDirectory) "Radium 2") + $radium2PathExists = (Test-Path $radium2Path) + + # The purpose of this is that when installing ORB, the only ones creating symlinks are SRE + # If we have created a symlink, that is intended to speed up the application deployment/startup process + # We should only remove symlinks if we aren't following the symlink strategy (reverting to a prior version/style of ORB typically) + # If we are using the symlink strategy, this reduces the working number of filesystem copies being invoked, which is a net-win for speed + # This also can obviate the need to delete the temporary asp.net files, possibly. + $isSymlinkStrategy = (Test-InstallerUseSymlinkStrategy) + Write-Host "$loglead : Is Symlink Strategy? [$isSymlinkStrategy]" + + if (!(Test-Path $OrbDirectory)) { + (New-Item $OrbDirectory -ItemType Directory) | Out-Null + Set-AppTierFolderAndFilePermissions + } + if (!(Test-Path $OrbDirectory)) { + throw "$logLead : $OrbDirectory folder not found - should have been created in this function" + } + + Write-Host "$logLead : Cleaning $OrbDirectory excluding log4net.config" + Remove-FileSystemItem $OrbDirectory -Recurse -Exclude "log4net.config" -Force -SkipSymlinks:$isSymlinkStrategy -Verbose:$VerbosePreference + + Write-Host "$logLead : Copy files from $TempDirectory to $OrbDirectory" + (Copy-Item $TempDirectory (Split-Path $OrbDirectory -Parent) -Recurse -Force) | Out-Null + + if($radium2PathExists){ + Write-Host "$logLead : Copy Radium to Radium 2..." + Write-Verbose "$logLead : Removing Radium2 folder" + (Remove-FileSystemItem $radium2Path -Recurse -Force -SkipSymlinks:$isSymlinkStrategy -Verbose:$VerbosePreference) | Out-Null + Write-Verbose "$logLead : Radium2 folder removed" + (New-Item $radium2Path -ItemType Directory -Force) | Out-Null + (Copy-Item (Join-Path ($OrbDirectory) "Radium\*") $radium2Path -Recurse -Force) | Out-Null + } else { + Write-Host ("$logLead : 'Radium 2' folder not found, skipping") + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Copy-LaunchConfiguration.ps1 b/Modules/Alkami.DevOps.Operations/Public/Copy-LaunchConfiguration.ps1 new file mode 100644 index 0000000..1c70cfd --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Copy-LaunchConfiguration.ps1 @@ -0,0 +1,90 @@ + +function Copy-LaunchConfiguration { +<# +.SYNOPSIS + Creates a new Launch Configuration in AWS based on an existing Launch Configuration. + +.DESCRIPTION + Will create a new Launch Configuration from an existing one. New LC will require the AMI ID that's passed in. + This exists because there is no way to edit or copy an existing Launch Configuration to update it + to a newer AMI. + +Returns one Amazon.AutoScaling.Model.LaunchConfiguration object. + +.PARAMETER launchConfig + [Amazon.AutoScaling.Model.LaunchConfiguration] Existing Launch Configuration object to copy. + +.PARAMETER imageId + [string] AMI ImageID as a string for adding to the new Launch Configuration. + +.PARAMETER newLaunchConfigName + [string] Name of the new Launch Configuration. Must be unique. + +.PARAMETER profileName + [string] AWS Profile. ("Sandbox", "prod") + +.PARAMETER awsRegion + [string] AWS Region. ("us-east-1") + +.EXAMPLE + Copy-LaunchConfiguration -launchConfig [Amazon.AutoScaling.Model.LaunchConfiguration]object -imageId "ami-06922c371ca793273" -newLaunchConfigName "0.0_Sandbox_web-2019082120433000000001" +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [Amazon.AutoScaling.Model.LaunchConfiguration]$launchConfig, + + [Parameter(Mandatory=$true)] + [string]$imageId, + + [Parameter(Mandatory=$true)] + [string]$newLaunchConfigName, + + [Parameter(Mandatory=$true)] + [string]$profileName, + + [Alias("Region")] + [Parameter(Mandatory=$true)] + [string]$awsRegion + ) + $logLead = (Get-LogLeadName) + + Import-AWSModule # AS + + # Create a new Launch Configuration object. + Write-Host "$logLead : Creating a new ASLaunchConfiguration $newLaunchConfigName with AMI $imageId from LaunchConfig $($launchConfig.LaunchConfigurationName)" + + Invoke-CommandWithRetry -Arguments ($profileName, $awsRegion, $newLaunchConfigName, $imageId, $launchConfig) -MaxRetries 3 -Exponential -ScriptBlock { + param($sbProfileName, $sbAwsRegion, $sbNewLaunchConfigName, $sbImageId, $sbLaunchConfig) + + New-ASLaunchConfiguration -ProfileName $sbProfileName -Region $sbAwsRegion ` + -LaunchConfigurationName $sbNewLaunchConfigName ` + -ImageId $sbImageId ` + -KeyName $sbLaunchConfig.KeyName ` + -SecurityGroup $sbLaunchConfig.SecurityGroups ` + -AssociatePublicIpAddress $sbLaunchConfig.AssociatePublicIpAddress ` + -BlockDeviceMapping $sbLaunchConfig.BlockDeviceMappings ` + -ClassicLinkVPCId $sbLaunchConfig.ClassicLinkVPCId ` + -ClassicLinkVPCSecurityGroup $sbLaunchConfig.ClassicLinkVPCSecurityGroups ` + -EbsOptimized $sbLaunchConfig.EbsOptimized ` + -InstanceMonitoring_Enabled $true ` + -IamInstanceProfile $sbLaunchConfig.IamInstanceProfile ` + -InstanceType $sbLaunchConfig.InstanceType ` + -KernelId $sbLaunchConfig.KernelId ` + -PlacementTenancy $sbLaunchConfig.PlacementTenancy ` + -RamdiskId $sbLaunchConfig.RamdiskId ` + -SpotPrice $sbLaunchConfig.SpotPrice ` + -UserData $sbLaunchConfig.UserData ` + } + + # Get the new launch configuration + $newLaunchConfig = ( + Invoke-CommandWithRetry -Arguments ($profileName, $awsRegion, $newLaunchConfigName) -MaxRetries 3 -Exponential -ScriptBlock{ + param($sbProfileName, $sbAwsRegion, $sbNewLaunchConfigName) + + return Get-ASLaunchConfiguration -ProfileName $sbProfileName -Region $sbAwsRegion -LaunchConfigurationName $sbNewLaunchConfigName + } + ) + Write-Host "$logLead : New Launch Configuration Name:$($newLaunchConfig.LaunchConfigurationName)" + return $newLaunchConfig +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Disable-Microservices.ps1 b/Modules/Alkami.DevOps.Operations/Public/Disable-Microservices.ps1 new file mode 100644 index 0000000..cb82b9c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Disable-Microservices.ps1 @@ -0,0 +1,53 @@ +function Disable-Microservices { +<# +.SYNOPSIS + Disables all alkami/SDK microservices, except for the untouchable broker, beacon, and subscription service. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [string[]]$microservicesToIgnore + ) + + $logLead = (Get-LogLeadName); + + Write-Host "$logLead : Now disabling microservice services." + + # We never want to disable these microservices automatically. + $ignoreList = @("Alkami.Services.Subscriptions.Host", "Alkami.MicroServices.Features.Beacon.Host", "Alkami.MicroServices.Broker.Host"); + $ignoreList += $microservicesToIgnore; + Write-Verbose "$logLead : Not Disabling Microservices: $ignoreList"; + + # Get list of chocolatey microservices. + $packages = Get-CategorizedChocoPackages + $microservicePackages = @() + $microservicePackages += $packages.WithMigrations + $microservicePackages += $packages.WithoutMigrations + + # Disable them all. + foreach($package in $microservicePackages) + { + # Skip packages in the ignore list. + if($ignoreList | Where-Object { $_ -eq $package.Name }) + { + Write-Verbose "$logLead : Skipping $($package.Name)" + continue; + } + + # Note: This returns $null if the service is already disabled, without the -includeDisabled flag. + $service = Get-ServiceByChocoName $package.Name + + if($service) + { + # Stop the service if it's running, and then disable it. + Stop-AlkamiService $service.Name + + Write-Verbose "$logLead : Disabling service for chocolatey package `"$($package.Name)`"" + Set-Service $service.Name -StartupType Disabled + } + else + { + Write-Verbose "$logLead : `"$($package.Name)`" is already disabled, or a service could not be found to disable." + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.ps1 b/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.ps1 new file mode 100644 index 0000000..a2db03f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.ps1 @@ -0,0 +1,44 @@ +function Disable-UnnecessaryMicroServices { + + <# +.SYNOPSIS + Stops and disables unnecessary microservices based on configured providers in a pod + +.DESCRIPTION + Stops and disables unnecessary microservices based on configured providers in a pod. Compares a Vanguard maintained package to provider list to all tenant providers, + by filtering out invalid providers (Deleted or Invalid Provider Type.) Stops identified superfluous services and sets their start mode to disabled. + +.PARAMETER ExcludePackageId + Array of PackageIds to exclude from being disabled. This will be combined with return value from Get-AlwaysEnabledProviderPackageIds + +.PARAMETER PrintDetailedResults + When specified, prints a detailed accounting of services acted upon + +.NOTES + SupportsShouldProcess enabled function. Supports both -WhatIf and -Confirm + +.LINK + Get-AlwaysEnabledProviderPackageIds +#> + + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Scope='Function', Justification="We want -WhatIf support passed down in to the underlying functions")] + param( + [Parameter(Mandatory = $false)] + [string[]]$ExcludePackageId = @(), + [Parameter(Mandatory = $false)] + [switch]$PrintDetailedResults + ) + + $logLead = Get-LogLeadName + + [array]$unmatchedProviders = Get-ProviderMappingFileUnion -OverrideEnabledProviderPackageIds $ExcludePackageId -ReturnUnmatched + + if (Test-IsCollectionNullOrEmpty $unmatchedProviders) { + + Write-Warning "$logLead : Found no unmatched providers. While possible, it is highly unlikely. Review the mapping file contents for issues" + return + } + + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchedProviders -Disable -PrintDetailedResults:$PrintDetailedResults +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.tests.ps1 new file mode 100644 index 0000000..c504924 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Disable-UnnecessaryMicroservices.tests.ps1 @@ -0,0 +1,53 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Disable-UnnecessaryMicroservices" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + + Context "Tests" { + + It "Writes a Warning and Exits Early if a Null is Returned" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + + Disable-UnnecessaryMicroServices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Found no unmatched providers." } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Exits Early if an Empty Array is Returned" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + + Disable-UnnecessaryMicroServices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Found no unmatched providers." } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 0 -Exactly -Scope It + } + + It "Calls the Set State Function with the Disable Flag" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return @( "how", "DARE", "you" ) } + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + + Disable-UnnecessaryMicroServices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Disable.IsPresent } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.ps1 b/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.ps1 new file mode 100644 index 0000000..bdaf64d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.ps1 @@ -0,0 +1,49 @@ +function Enable-NecessaryMicroservices { + + <# +.SYNOPSIS + Enables necessary microservices based on configured providers in a pod + +.DESCRIPTION + Starts and enables necessary microservices based on configured providers in a pod. Compares a Vanguard maintained package to provider list to all tenant providers, + by filtering out invalid providers (Deleted or Invalid Provider Type.) Starts identified services and sets their start mode to enabled. + +.PARAMETER IncludePackageId + Array of PackageIds to include in being enabled. This will be combined with return value from Get-AlwaysEnabledProviderPackageIds + +.PARAMETER PrintDetailedResults + When specified, prints a detailed accounting of services acted upon + +.NOTES + SupportsShouldProcess enabled function. Supports both -WhatIf and -Confirm + +.LINK + Get-AlwaysEnabledProviderPackageIds +#> + + [CmdletBinding(SupportsShouldProcess)] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Scope='Function', Justification="We want -WhatIf support passed down in to the underlying functions")] + param( + [Parameter(Mandatory = $false)] + [string[]]$IncludePackageId = @(), + [Parameter(Mandatory = $false)] + [switch]$PrintDetailedResults + ) + + $logLead = Get-LogLeadName + + #$alwaysEnabledPackageIds = Get-AlwaysEnabledProviderPackageIds + #[array]$packagesToForceEnabling = $IncludePackageId + $alwaysEnabledPackageIds + + [array]$matchedProviders = Get-ProviderMappingFileUnion -OverrideEnabledProviderPackageIds $IncludePackageId -ReturnMatched + + $unmatchedProviders = $unmatchedProviders.Where({ $_.PackageId -notin $packagesToSkipDisabling }) + + if (Test-IsCollectionNullOrEmpty $matchedProviders) { + + Write-Warning "$logLead : Found no provider microservices that need to be enabled based on tenant configuration. Exiting." + return + } + + Set-MicroserviceConfigurationBasedState -ProviderData $matchedProviders -Enable -PrintDetailedResults:$PrintDetailedResults +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.tests.ps1 new file mode 100644 index 0000000..92ecbe3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Enable-NecessaryMicroservices.tests.ps1 @@ -0,0 +1,53 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Enable-NecessaryMicroservices" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + + Context "Tests" { + + It "Writes a Warning and Exits Early if a Null is Returned" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + + Enable-NecessaryMicroservices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Found no provider microservices that need to be enabled" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Exits Early if an Empty Array is Returned" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + + Enable-NecessaryMicroservices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Found no provider microservices that need to be enabled" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 0 -Exactly -Scope It + } + + It "Calls the Set State Function with the Disable Flag" { + + Mock -CommandName Get-AlwaysEnabledProviderPackageIds -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-ProviderMappingFileUnion -ModuleName $moduleForMock -MockWith { return @( "how", "DARE", "you" ) } + Mock -CommandName Set-MicroserviceConfigurationBasedState -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + + Enable-NecessaryMicroservices + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-MicroserviceConfigurationBasedState -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Enable.IsPresent } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Format-IpAddress.ps1 b/Modules/Alkami.DevOps.Operations/Public/Format-IpAddress.ps1 new file mode 100644 index 0000000..9e1881a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Format-IpAddress.ps1 @@ -0,0 +1,17 @@ +Function Format-IpAddress { + <# +.SYNOPSIS + When given a ip address string, returns type System.Net.IPAddress. +.EXAMPLE + Close-TcpSocket -Socket $Socket +.INPUTS + Socket +.NOTES + This does not validate that the input IP is a valid dotted-quad address. see https://docs.microsoft.com/en-us/dotnet/api/system.net.ipaddress.parse?view=netcore-3.1#System_Net_IPAddress_Parse_System_String_ + Example: Format-IpAddress -Ip "127.1" translates to 127.0.0.1 +#> + [CmdletBinding()] + [OutputType([System.Net.IPAddress])] + param($Ip) + return [System.Net.IPAddress]::Parse($Ip) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.ps1 b/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.ps1 new file mode 100644 index 0000000..eeffbd8 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.ps1 @@ -0,0 +1,40 @@ + +function Format-SlackMessage { + <# + .SYNOPSIS + Format a Slack message. + .DESCRIPTION + Used to format the slack message body for use by Publish-MessageToSlack + .EXAMPLE + $messageText = "$environmentName [$Server] - Removed from load balancer" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName "Rolling Patching" + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + .INPUTS + [Parameter(Mandatory = $true)] + [string] $MessageText + [string] $IconEmoji + [string] $UserName + .OUTPUTS + [hashtable] Message Body + .NOTES + used a lot in Invoke-RollingScriptBlock + #> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory = $true)] + [string] $MessageText, + [string] $IconEmoji, + [string] $UserName + ) + + #Build a message for Slack. + $messageBody = @{ + + "username" = "$UserName"; + "text" = "$MessageText"; + "icon_emoji" = "$IconEmoji"; + } + + return $messageBody +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.tests.ps1 new file mode 100644 index 0000000..a3d947c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Format-SlackMessage.tests.ps1 @@ -0,0 +1,39 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Format-SlackMessage" { + Context "Returns expected value" { + It "username provided as string" { + $expectedValue = "expectedValue" + $param = "username" + (Format-SlackMessage -MessageText $expectedValue -UserName $expectedValue)[$param] | Should -Be $expectedValue + } + It "MessageText provided as string" { + $expectedValue = "expectedValue" + $param = "text" + (Format-SlackMessage -MessageText $expectedValue)[$param] | Should -Be $expectedValue + } + It "icon_emoji provided as string" { + $expectedValue = "expectedValue" + $param = "icon_emoji" + (Format-SlackMessage -MessageText $expectedValue -IconEmoji $expectedValue)[$param] | Should -Be $expectedValue + } + } + Context "ensurses mandatory value is supplied " { + It "MessageText is supplied" { + $expectedValue = "expectedValue" + $param = "text" + (Format-SlackMessage -MessageText $expectedValue)[$param] | Should -Be $expectedValue + } + It "MessageText is not supplied" { + $expectedValue = $null + $param = "text" + {(Format-SlackMessage -MessageText $expectedValue)[$param]} | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.ps1 new file mode 100644 index 0000000..7671e5c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.ps1 @@ -0,0 +1,117 @@ +function Get-ASInstanceHealth { +<# +.SYNOPSIS + Gets an EC2 Instance's health state from the ASG. This function can only be run against an AWS server. + +.DESCRIPTION + Gets an EC2 Instance's health from the ASG. May run against a single remote machine. If no machine name parameter is specified, executes against localhost. + This function can only be run against an AWS server. + +.PARAMETER ComputerName + [string] Optional FQDN of the machine to retrieve ASG status. Defaults to the local machine hostname. + +.PARAMETER InstanceId + [string] Optional known instance id. + +.PARAMETER ProfileName + [string] Optional profile name. Preferred that it be provided. + +.PARAMETER Region + [string] Optional region name. Preferred that it be provided. + +.EXAMPLE + Get-ASInstanceHealth + + [Get-ASInstanceHealth] : Attempting to Get Computer WEB1636121 Instance Health status. + [Get-ASInstanceHealth] : Computer WEB1636121 Instance Health status: HEALTHY + +.EXAMPLE + Get-ASInstanceHealth -ComputerName 'APP1611693.fh.local' -ProfileName Prod + + [Get-ASInstanceHealth] : Computer APP1611693.fh.local [InstanceId: i-0fa327b1d33fc2aab] Health status: UNHEALTHY. + UNHEALTHY +#> + + [CmdletBinding(DefaultParameterSetName="ComputerName")] + [OutputType([string])] + param( + [Alias("MachineName", "ServerName")] + [Parameter(Mandatory = $false, ParameterSetName = "ComputerName")] + [string]$ComputerName = $null, + + [Parameter(Mandatory = $true, ParameterSetName = "InstanceId")] + [string]$InstanceId = $null, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + ) + + $logLead = Get-LogLeadName + + $errorReturn = 'Unknown' + + Import-AWSModule # AS + + if ([string]::IsNullOrWhiteSpace($ComputerName) -and [string]::IsNullOrWhiteSpace($InstanceId)) { + $ComputerName = (Get-FullyQualifiedServerName) + } + + # If no hostname, but we do have an instance id, we can get the hostname from the instance tags + if ([string]::IsNullOrWhiteSpace($ComputerName) -and ![string]::IsNullOrWhiteSpace($InstanceId)) { + $regions = Get-SupportedAwsRegions + foreach ($region in ($regions)) { + try { + $instance = (Get-EC2Instance -InstanceId $InstanceId -Region $region -ProfileName $ProfileName -ErrorAction SilentlyContinue).Instances + if ($null -ne $instance) { + $ComputerName = $instance.Tags.Where( { $_.Key -eq 'alk:hostname' }).Value + Write-Host "$logLead : Resolved hostname by tag to [$ComputerName]" + break + } + } catch { + <#I literally don't care, this is a loop. There's an error check for empty below. #> + Write-Verbose "$logLead : Could not find instance in Region [$region] - This just means it could be in any of [$($regions -join ',')]" + } + } + } + + # If no instanceid, we can get the instanceid from the hostname + if ([string]::IsNullOrWhiteSpace($InstanceId)) { + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + throw "$logLead : Neither -ComputerName [$ComputerName] nor -InstanceId [$InstanceId] have useful parameters to determine state. Please provide appropriate parameters." + } + $InstanceId = @(Get-EC2InstancesByHostname -Servers $ComputerName -ProfileName $ProfileName)[0].InstanceId + Write-Host "$logLead : Resolved [$ComputerName] to InstanceId [$InstanceId]" + } + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + throw "$logLead : Unable to determine ComputerName for InstanceId [$InstanceId]" + } + + if ([string]::IsNullOrWhiteSpace($InstanceId)) { + throw "$logLead : Could not retrieve the instance id for computer: [$ComputerName]. Returning `$null" + } + + $instanceRegion = $Region + + if ([string]::IsNullOrWhiteSpace($instanceRegion)) { + $instanceRegion = (Get-AwsRegionByHostname -ComputerName $ComputerName) + } + + Write-Verbose "$logLead : Getting Current AutoScalingInstance." + $autoScalingInstance = (Get-ASAutoScalingInstance -InstanceID $InstanceId -Region $instanceRegion -ProfileName $ProfileName) + + if ($null -eq $autoScalingInstance) { + Write-Warning "$logLead : Could not retrieve the parent ASG Instance with ID $InstanceId." + return $errorReturn + } + + $asgName = $autoScalingInstance.AutoScalingGroupName + $health = $autoScalingInstance.HealthStatus + Write-Verbose "$logLead : ASG $asgName Reports Instance $currentInstanceId is $health." + + Write-Host "$logLead : Computer $ComputerName [InstanceId: $InstanceId] Health status: $health." + return $health +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.tests.ps1 new file mode 100644 index 0000000..554cc1f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceHealth.tests.ps1 @@ -0,0 +1,87 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ASInstanceHealth" { + Mock -ModuleName $moduleForMock -CommandName Get-EC2InstancesByHostname -MockWith { return @(@{InstanceId = "dummyid"; Tags = @(@{ Key = "alk:hostname"; Value = "hostname"; }) }) } + Mock -ModuleName $moduleForMock -CommandName Get-EC2Instance -MockWith { + return @{ + Instances = @( + @{ + InstanceId = "dummyid"; + Tags = @( + @{ Key = "alk:hostname"; Value = "hostname"; } + ) + } + ) + } + } + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "hostname" } + Mock -ModuleName $moduleForMock -CommandName Get-ASAutoScalingInstance -MockWith { return @{ LifecycleState = "garbage"; AutoScalingGroupName = "AutoScalingGroupName"; } } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + Mock -ModuleName $moduleForMock -CommandName Import-AWSModule -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Get-AwsRegionByHostname -MockWith { return "al-kami-1" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { <# NOOP #> } + + Context "Error Handling" { + + $expectedValue = 'Unknown' + + It "Writes a warning and returns Unknown if instance ID could not be retrieved" { + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return $null } + + $result = Get-ASInstanceHealth + + Assert-MockCalled -CommandName Get-EC2InstancesByHostname -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "Writes a warning and returns Unknown if ASG could not be retrieved" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return $null } + + $result = Get-ASInstanceHealth + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Could not retrieve the parent ASG Instance with ID" } + + $result | Should -Be $expectedValue + } + } + + Context "Status Mappings" { + + It "UNHEALTHY status maps to UNHEALTHY" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ HealthStatus = 'UNHEALTHY'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'UNHEALTHY' + $result = Get-ASInstanceHealth + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "HEALTHY status maps to HEALTHY" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ HealthStatus = 'HEALTHY'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'HEALTHY' + $result = Get-ASInstanceHealth + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.ps1 new file mode 100644 index 0000000..c832c3b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.ps1 @@ -0,0 +1,120 @@ +function Get-ASInstanceState { +<# +.SYNOPSIS + Gets an EC2 Instance state from the ASG. This function can only be run against an AWS server. + +.DESCRIPTION + Gets an EC2 Instance state from the ASG. May run against a single remote machine. If no machine name parameter is specified, executes against localhost. + This function can only be run against an AWS server. + +.PARAMETER ComputerName + [string] Optional FQDN of the machine to retrieve ASG status. Defaults to the local machine hostname. + +.PARAMETER InstanceId + [string] Optional known instance id. + +.PARAMETER ProfileName + [string] Optional profile name. Preferred that it be provided. + +.PARAMETER Region + [string] Optional region name. Preferred that it be provided. + +.EXAMPLE + Get-ASInstanceState + + [Get-ASInstanceState] : Attempting to Get Computer WEB1636121 ASG status. + [Get-ASInstanceState] : Computer WEB1636121 ASG status: Active + +.EXAMPLE + Get-ASInstanceState -ComputerName 'APP1611693.fh.local' -ProfileName Prod + + [Get-ASInstanceState] : Computer APP1611693.fh.local [InstanceId: i-0fa327b1d33fc2aab] ASG status: Active. + Active +#> + + [CmdletBinding(DefaultParameterSetName="ComputerName")] + [OutputType([string])] + param( + [Alias("MachineName", "ServerName")] + [Parameter(Mandatory = $false, ParameterSetName = "ComputerName")] + [string]$ComputerName = $null, + + [Parameter(Mandatory = $true, ParameterSetName = "InstanceId")] + [string]$InstanceId = $null, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + + ) + + $logLead = Get-LogLeadName + + $errorReturn = 'Unknown' + + Import-AWSModule # AS + + if ([string]::IsNullOrWhiteSpace($ComputerName) -and [string]::IsNullOrWhiteSpace($InstanceId)) { + $ComputerName = (Get-FullyQualifiedServerName) + } + + # If no hostname, but we do have an instance id, we can get the hostname from the instance tags + if ([string]::IsNullOrWhiteSpace($ComputerName) -and ![string]::IsNullOrWhiteSpace($InstanceId)) { + $regions = Get-SupportedAwsRegions + foreach ($region in ($regions)) { + try { + $instance = (Get-EC2Instance -InstanceId $InstanceId -Region $region -ProfileName $ProfileName -ErrorAction SilentlyContinue).Instances + if ($null -ne $instance) { + $ComputerName = $instance.Tags.Where( { $_.Key -eq 'alk:hostname' }).Value + Write-Host "$logLead : Resolved hostname by tag to [$ComputerName]" + break + } + } catch { + <#I literally don't care, this is a loop. There's an error check for empty below. #> + Write-Verbose "$logLead : Could not find instance in Region [$region] - This just means it could be in any of [$($regions -join ',')]" + } + } + } + + # If no instanceid, we can get the instanceid from the hostname + if ([string]::IsNullOrWhiteSpace($InstanceId)) { + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + throw "$logLead : Neither -ComputerName [$ComputerName] nor -InstanceId [$InstanceId] have useful parameters to determine state. Please provide appropriate parameters." + } + $InstanceId = @(Get-EC2InstancesByHostname -Servers $ComputerName -ProfileName $ProfileName)[0].InstanceId + Write-Host "$logLead : Resolved [$ComputerName] to InstanceId [$InstanceId]" + } + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + throw "$logLead : Unable to determine ComputerName for InstanceId [$InstanceId]" + } + + if ([string]::IsNullOrWhiteSpace($InstanceId)) { + throw "$logLead : Could not retrieve the instance id for computer: [$ComputerName]. Returning `$null" + } + + $instanceRegion = $Region + + if ([string]::IsNullOrWhiteSpace($instanceRegion)) { + $instanceRegion = (Get-AwsRegionByHostname -ComputerName $ComputerName) + } + + Write-Verbose "$logLead : Getting Current AutoScalingInstance." + $autoScalingInstance = (Get-ASAutoScalingInstance -InstanceID $InstanceId -Region $instanceRegion -ProfileName $ProfileName) + + if ($null -eq $autoScalingInstance) { + Write-Warning "$logLead : Could not retrieve the parent ASG Instance with ID $InstanceId." + return $errorReturn + } + + $lifecycleStatus = $autoScalingInstance.LifecycleState + $asgName = $autoScalingInstance.AutoScalingGroupName + Write-Verbose "$logLead : ASG $asgName Reports Instance $currentInstanceId is $lifecycleStatus." + + $result = ConvertTo-NormalizedLoadBalancerState "AWS" -InputState $lifecycleStatus + + Write-Host "$logLead : Computer $ComputerName [InstanceId: $InstanceId] ASG status: $result." + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.tests.ps1 new file mode 100644 index 0000000..be17332 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ASInstanceState.tests.ps1 @@ -0,0 +1,123 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ASInstanceState" { + Mock -ModuleName $moduleForMock -CommandName Get-EC2InstancesByHostname -MockWith { return @(@{InstanceId = "dummyid"; Tags = @(@{ Key = "alk:hostname"; Value = "hostname"; }) }) } + Mock -ModuleName $moduleForMock -CommandName Get-EC2Instance -MockWith { + return @{ + Instances = @( + @{ + InstanceId = "dummyid"; + Tags = @( + @{ Key = "alk:hostname"; Value = "hostname"; } + ) + } + ) + } + } + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "hostname" } + Mock -ModuleName $moduleForMock -CommandName Get-ASAutoScalingInstance -MockWith { return @{ LifecycleState = "garbage"; AutoScalingGroupName = "AutoScalingGroupName"; } } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + Mock -ModuleName $moduleForMock -CommandName Import-AWSModule -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Get-AwsRegionByHostname -MockWith { return "al-kami-1" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { <# NOOP #> } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { <# NOOP #> } + + Context "Error Handling" { + + $expectedValue = 'Unknown' + + It "Writes a warning and returns Unknown if instance ID could not be retrieved" { + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return $null } + + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-EC2InstancesByHostname -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "Writes a warning and returns Unknown if ASG could not be retrieved" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return $null } + + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Could not retrieve the parent ASG Instance with ID" } + + $result | Should -Be $expectedValue + } + } + + Context "Status Mappings" { + + It "Pending status maps to ActivePending" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ LifecycleState = 'Pending'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'ActivePending' + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "InService status maps to Active" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ LifecycleState = 'InService'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'Active' + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "EnteringStandby status maps to StandbyPending" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ LifecycleState = 'EnteringStandby'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'StandbyPending' + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "Standby status maps to Standby" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return @{ LifecycleState = 'Standby'; AutoScalingGroupName = 'Test' } } + + $expectedValue = 'Standby' + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + + It "Unmapped status maps to Unknown" { + + Mock -CommandName Get-ASAutoScalingInstance -ModuleName $moduleForMock -MockWith { return $null } + + $expectedValue = 'Unknown' + $result = Get-ASInstanceState + + Assert-MockCalled -CommandName Get-ASAutoScalingInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + $result | Should -Be $expectedValue + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-AllLoadBalancerStates.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-AllLoadBalancerStates.ps1 new file mode 100644 index 0000000..35de558 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-AllLoadBalancerStates.ps1 @@ -0,0 +1,99 @@ +function Get-AllLoadBalancerStates { +<# +.SYNOPSIS + Given a list of servers, fetch all the load balancer states and display them in a human readable format + +.PARAMETER ServerList + [string[]] This is a list of servernames to connect to for resetting + +.PARAMETER ProfileName + [string] AWS Profile name + +.PARAMETER Region + [string] AWS Region +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [string[]]$ServerList, + [string]$ProfileName, + [string]$Region + ) + + # if there are no servers, skip it! + if(Test-IsCollectionNullOrEmpty $ServerList) + { + Write-Host "No servers to act on. Skipping step." + return + } + + #Loop over the servers and get Load Balancer to reflect current on status of machine. + $sb = { + param ($hostname, $arguments) + + $isServerOn = (Test-ComputerIsAvailable -ComputerName $hostname) + $state = 'down' + if ($isServerOn) { + $state = 'up' + } + $lbState = (Get-LoadBalancerState -server $hostname -AwsProfileName $arguments.ProfileName -AwsRegion $arguments.Region) + + return @{ Server = $hostname; OnOffState = $state; LoadBalancerState = $lbState; } + } + + $results = (Invoke-Parallel2 -Objects $ServerList -Arguments @{ProfileName = $ProfileName; Region = $Region;} -Script $sb).Result + + $allUps = @($results.Where({$_.OnOffState -eq 'up' -and $_.LoadBalancerState -eq 'Active'})) + $allUpButStandby = @($results.Where({$_.OnOffState -eq 'up' -and $_.LoadBalancerState -ne 'Active'})) + + $allDowns = @($results.Where({$_.OnOffState -eq 'down' -and $_.LoadBalancerState -eq 'Standby'})) + $allDownButNotStandby = @($results.Where({$_.OnOffState -eq 'down' -and $_.LoadBalancerState -ne 'Standby'})) + + if ($allUps.Count -gt 0) { + Write-Host "==================================================================" + Write-Host "These servers all appear fine (online, active)" + foreach ($result in $allUps) { + $hostname = $result.Server + $state = $result.OnOffState + $lbState = $result.LoadBalancerState + Write-Host "Server: [$hostname] is power-status [$state] and lb-status [$lbState]" + } + } + + if ($allDowns.Count -gt 0) { + Write-Host "==================================================================" + Write-Host "These servers are all offline but not active, so probably fine" + foreach ($result in $allDowns) { + $hostname = $result.Server + $state = $result.OnOffState + $lbState = $result.LoadBalancerState + Write-Host "Server: [$hostname] is power-status [$state] and lb-status [$lbState]" + } + } + + if ($allUpButStandby.Count -gt 0) { + Write-Host "==================================================================" + Write-Host "These servers appear problematic (online, NOT active)" + foreach ($result in $allUpButStandby) { + $hostname = $result.Server + $state = $result.OnOffState + $lbState = $result.LoadBalancerState + Write-Host "Server: [$hostname] is power-status [$state] and lb-status [$lbState]" + } + Write-Host "`$servers = @(`"$($allUpButStandby.Server -join '","')`")" + } + + if ($allDownButNotStandby.Count -gt 0) { + Write-Host "==================================================================" + Write-Host "These servers appear problematic (NOT online, active)" + foreach ($result in $allDownButNotStandby) { + $hostname = $result.Server + $state = $result.OnOffState + $lbState = $result.LoadBalancerState + Write-Host "Server: [$hostname] is power-status [$state] and lb-status [$lbState]" + } + Write-Host "`$servers = @(`"$($allDownButNotStandby.Server -join '","')`")" + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-AlwaysEnabledProviderPackageIds.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-AlwaysEnabledProviderPackageIds.ps1 new file mode 100644 index 0000000..f768f13 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-AlwaysEnabledProviderPackageIds.ps1 @@ -0,0 +1,21 @@ +function Get-AlwaysEnabledProviderPackageIds { + <# + .SYNOPSIS + Returns array of Provider PackageIds (names) that should always be Enabled + + #> + [CmdletBinding()] + [OutputType([string[]])] + param ( + ) + $loglead = Get-LogLeadName + + [array]$alwaysEnabledProviderPackageIds = @( + "Alkami.MicroServices.Features.Beacon.Host", + "Alkami.MicroServices.ApplicationSettings.Service.Host" + ) + + Write-Host "$loglead - Returning Providers to always keep Enabled - $($alwaysEnabledProviderPackageIds -join ",")" + + return $alwaysEnabledProviderPackageIds +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.ps1 new file mode 100644 index 0000000..170764c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.ps1 @@ -0,0 +1,69 @@ +function Get-AwsRegionByHostname { +<# +.SYNOPSIS + Returns the AWS region a hostname is in by the subnet it is in. This is a best-guess! + This function is not guaranteed to give you the correct result, particularly if this function goes un-maintained when new regions are added or subnets are changed. + +.PARAMETER ComputerName + The server hostname to guess what AWS region it is in. +#> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory=$true)] + [string]$ComputerName + ) + + # TODO: Opportunity for improvement, if we pass in $ProfileName we could use Get-SupportedAwsRegions and iterate them with Get-EC2Instance to find the hostname + + $usEast = "us-east-1" + $usWest = "us-west-2" + + # This array needs maintenance when new subnets are added, if this function + # is to be expected to work for instances in said subnets + $usWestRanges = @( + "32", # DR West + "35", # Prod West + "36", # Dev West + "46" # Mgmt West + ) + + $logLead = Get-LogLeadName + + if (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName) { + Write-Verbose "$logLead : Parameter $ComputerName matches local identifiers. Returning current instance region." + return (Get-CurrentInstanceRegion) + } + + # Get the IP of the ComputerName + $dns = (Resolve-DnsName $ComputerName -Type A) + if($null -eq $dns) { + + Write-Warning "$logLead : Could not resolve DNS for host $ComputerName" + return $null + } + + if ($dns.Count -gt 1) { + + Write-Warning "$logLead : More than one IPv4 address resolved. Results may be incorrect -- we will take the first IP matching ^10." + } + + $ipAddress = ($dns.IPAddress | Where-Object { $_ -match "^10" } | Select-Object -First 1) + + if ($null -eq $ipAddress) { + + Write-Warning "$logLead : No IPv4 addresses found matching ^10." + return $null + } + + # Match the first octet of the IP address to a region subnet. + $region = $null + $secondOctet = $ipAddress.Split(".")[1] + if ($usWestRanges -contains $secondOctet) { + $region = $usWest + } else { + $region = $usEast + } + + return $region +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.tests.ps1 new file mode 100644 index 0000000..37bc343 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-AwsRegionByHostname.tests.ps1 @@ -0,0 +1,169 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AwsRegionByHostname" { + + Context "Returns Expected Values" { + + Mock Resolve-DnsName { + param($Name) + + $ips = @{ + # US-East-1 + MIC1 = "10.16.0.6" + MIC2 = "10.27.0.7" + + # US-West-2 + MIC3 = "10.32.0.6" + MIC4 = "10.35.0.7" + MIC5 = "10.36.0.8" + MIC6 = "10.46.0.9" + + # Wildcard US-East-1 + MIC7 = "10.42.0.42" + } + $ip = $ips[$Name] + return @{ IPAddress = $ip } + } -ModuleName $moduleForMock + + It "Returns US-East-1 on 16 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC1" | Should -BeExactly "us-east-1" + } + + It "Returns us-west-2 on 27 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC2" | Should -BeExactly "us-east-1" + } + + It "Returns us-west-2 on 32 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC3" | Should -BeExactly "us-west-2" + } + + It "Returns us-west-2 on 35 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC4" | Should -BeExactly "us-west-2" + } + + It "Returns us-west-2 on 36 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC5" | Should -BeExactly "us-west-2" + } + + It "Returns us-west-2 on 46 subnets." { + Get-AwsRegionByHostname -COMPUTERNAME "MIC6" | Should -BeExactly "us-west-2" + } + + It "Defaults to us-east-1 for every other IP" { + Get-AwsRegionByHostname -COMPUTERNAME "MIC7" | Should -BeExactly "us-east-1" + } + } + + Context "Edge Cases" { + + It "Only uses IPv4 Addresses that Start with 10" { + + Mock Resolve-DnsName { + + $record1 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "8.8.8.8"; + } + + $record2 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "10.32.0.1"; + } + + return @($record1, $record2) + + } -ModuleName $moduleForMock + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Get-AwsRegionByHostname "Foo.Bar.Com" | Should -BeExactly "us-west-2" + } + + It "Writes a Warning if More than One IPv4 Address Found" { + + Mock Resolve-DnsName { + + $record1 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "10.16.0.6"; + } + + $record2 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "8.8.8.8"; + } + + return @($record1, $record2) + + } -ModuleName $moduleForMock + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Get-AwsRegionByHostname "Foo.Bar.Com" + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Scope It -Times 1 -Exactly ` + -ParameterFilter { $Message -match "More than one IPv4 address resolved" } + } + + It "Writes a Warning and Returns Null if No IPv4 Addresses Match ^10." { + + Mock Resolve-DnsName { + + $record1 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "8.8.4.4"; + } + + $record2 = New-Object PSObject -Property @{ + Name = "Foo.Bar.Com"; + IPAddress = "8.8.8.8"; + } + + return @($record1, $record2) + + } -ModuleName $moduleForMock + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Get-AwsRegionByHostname "Foo.Bar.Com" | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Scope It -Times 1 -Exactly ` + -ParameterFilter { $Message -match "No IPv4 addresses found" } + } + + It "Writes a Warning and Returns Null if the Hostname Cannot be Resolved" { + + Mock Resolve-DnsName { + + return $null + + } -ModuleName $moduleForMock + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Get-AwsRegionByHostname "Foo.Bar.Com" | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Scope It -Times 1 -Exactly ` + -ParameterFilter { $Message -match "Could not resolve DNS for host" } + } + } + + Context "Local Execution" { + + Mock Get-CurrentInstanceRegion { + return "eu-northwestish-99" + } -ModuleName $moduleForMock + + Mock Resolve-DnsName {} -ModuleName $moduleForMock + + It "Calls Get-CurrentInstanceRegion When Hostname Matches Local Identifiers" { + + Get-AwsRegionByHostname -COMPUTERNAME $ENV:COMPUTERNAME | Should -BeExactly "eu-northwestish-99" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CurrentInstanceRegion -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Resolve-DnsName -Times 0 -Exactly -Scope It + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-BadLogConfiguration.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-BadLogConfiguration.ps1 new file mode 100644 index 0000000..2aeb658 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-BadLogConfiguration.ps1 @@ -0,0 +1,158 @@ +function Get-BadLogConfiguration { + + <# +.SYNOPSIS + Scans Chocolatey and ORB log4net Configuration for Known Issues + +.DESCRIPTION + Scans Chocolatey and ORB log4net Configuration for Known Issues, including ROOT at TRACE/DEBUG/INFO/WARN, + or namespaces at too verbose a level. Most parameters are configurable. + +.PARAMETER IncludeTracedMessageDetail + Namespaces defined in parameter CollapsedNameSpaces are summarized by default, due to the large number. Pass this switch to return detail on those namespaces. + +.PARAMETER AllowedRootLogLevels + An array of allowable log levels for the ROOT namespace + +.PARAMETER UnwantedLogLevels + Log levels for non-ROOT namespaces which are considered too verbose. + +.PARAMETER CollapsedNameSpaces + Namespaces to collapse and which will only return a total count of log files with undesired verbosity. + +.Example + Get-BadLogConfiguration + +.Example + Get-BadLogConfiguration -IncludeTracedMessageDetail + +.Example + Get-BadLogConfiguration -AllowedRootLogLevels @("FATAL","ERROR","WARN") + +.Example + Get-BadLogConfiguration -UnwantedLogLevels @("ALL","TRACE","DEBUG") + +.Example + Get-BadLogConfiguration -CollapsedNameSpaces @("Alkami.MicroServices.Blabbermouth","Alkami.DeepState") +#> + + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Switch]$IncludeTracedMessageDetail, + + [Parameter(Mandatory = $false)] + [string[]]$AllowedRootLogLevels = @("ERROR", "FATAL"), + + [Parameter(Mandatory = $false)] + [string[]]$UnwantedLogLevels = @("ALL", "TRACE"), + + [Parameter(Mandatory = $false)] + [string[]]$CollapsedNameSpaces = @("Alkami.MicroServices.TracedMessages", "Alkami.ExternalServices.TracedMessages", "Alkami.TracedSql") + ) + + $logLead = Get-LogLeadName + [array]$log4nets = @() + + $chocoPath = Join-Path -Path (Get-ChocolateyInstallPath) -ChildPath "lib" + if (Test-Path $chocoPath) { + + $log4nets += Get-ChildItem -Path $chocoPath -Recurse -File -Filter log4net.config -Depth 2 + } + + $orbPath = Get-OrbPath + if (Test-Path ($orbPath)) { + + $log4nets += Get-ChildItem -Path $orbPath -Recurse -File -Filter log4net.config -Depth 2 + } + + if (Test-IsCollectionNullOrEmpty $log4nets) { + + Write-Warning "$logLead : Unable to find any log4net config files. Is this running on a non-ORB server?" + return + } + + # These may number in the hundreds, and may be benign, so lets just get the counts unless the user asks for them. + # We'll make an object and attach a property to each + $collapsedNameSpaceObjects = @() + foreach ($namespaceToCollapse in $CollapsedNameSpaces) { + + $collapsedNameSpaceObjects += New-Object PSObject -Property @{ + NameSpace = $namespaceToCollapse; + Count = 0; + } + } + + foreach ($config in $log4nets) { + + if ($config.FullName -like "$chocoPath*") { + + $applicationName = (Get-Item -Path $config.FullName).Directory.Parent.Name + + if (@("tools", "app", "lib") -contains $applicationName) { + + $applicationName = (Get-Item -Path $config.FullName).Directory.Parent.Parent.Name + } + } elseif ($config.FullName -like "$orbPath*") { + + $applicationName = (Get-Item -Path $config.FullName).Directory.Name + + } else { + + Write-Warning "$logLead : Unknown application for log configuration at $($config.FullName)" + } + + [xml]$configXML = Read-XmlFile -xmlPath $config.FullName + $log4netConfig = $configXML.configuration.log4net + + foreach ($logger in $log4netConfig.logger) { + + $logLevel = $logger.Level + $namespace = $logger.Name + + $logValue = $null + if ($null -ne $logLevel -and $null -ne $logLevel.value) { + + $logValue = $logLevel.value + } + + if ($UnwantedLogLevels -contains $logValue) { + + if ([string[]]($collapsedNameSpaceObjects.NameSpace) -contains $namespace) { + + ($collapsedNameSpaceObjects | Where-Object { $_.NameSpace -eq $namespace }).Count++ + if ($IncludeTracedMessageDetail.IsPresent) { + + Write-Host "[$applicationName]: $namespace - $logValue" + } + + } else { + + Write-Host "[$applicationName]: $namespace - $logValue" + } + } elseif (("ROOT" -eq $namespace) -and ($AllowedRootLogLevels -notcontains $logLevel)) { + + # Handles Inline Root Namespaces + $badRootLogLevelCount++ + Write-Host "[$applicationName]: $namespace - $logValue" + } + } + + if ($null -ne $configXML.configuration.log4net.root) { + + # Handles Root as a Discrete Child Node of log4net + $rootNode = $configXML.configuration.log4net.root + if (($null -ne $rootNode.level) -and ($AllowedRootLogLevels -notcontains $rootNode.level.value)) { + + $badRootLogLevelCount++ + Write-Host "[$applicationName]: ROOT - $($rootNode.level.value)" + } + } + } + + foreach ($summaryNameSpace in $collapsedNameSpaceObjects) { + + Write-Warning "$logLead : $($summaryNameSpace.Count) Log Configs have $($summaryNameSpace.NameSpace) at an unsupported log level" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-BounceJobBuildId.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-BounceJobBuildId.ps1 new file mode 100644 index 0000000..17e28f7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-BounceJobBuildId.ps1 @@ -0,0 +1,48 @@ +function Get-BounceJobBuildId { + <# + .SYNOPSIS + Returns the TeamCity BuildId of a "Bounce Services" job, based on naming convention. + The convention is "DeployV3___BounceServices" + Returns `$null if it cannot determine which template to use for generating the build id. + + .PARAMETER EnvironmentType + The environment type to target - "Dev", "Qa", "Staging", "Production", etc + CASE SENSITIVE - Must match the BuildId in TeamCity. + + .PARAMETER EnvironmentName + The environment name to target - "Red7", "Morph", "Ci1" + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet("Dev","Sandbox","Qa","Staging","Production","DisasterRecovery","Pilot","LoadTest")] + [string]$EnvironmentType, + [Parameter(Mandatory=$true)] + [string]$EnvironmentName + ) + $loglead = (Get-LogLeadName) + $environmentTypesWithSharedBounceJob = @("Staging","Production","DisasterRecovery","Pilot","LoadTest") + $environmentTypesWithSeparateBounceJob = @("Dev","Sandbox","Qa") + + $bounceJobBuildId = $null + $bounceJobBuildIdTemplate = "REPLACEME" + $bounceJobBuildIdPrefix = "DeployV3" + $bounceJobBuildIdSuffix = "BounceServices" + $buildEnvironmentType = $EnvironmentType + $buildEnvironmentName = $EnvironmentName + + if ($EnvironmentType -in $environmentTypesWithSeparateBounceJob) { + $bounceJobBuildIdTemplate = "{0}_{1}_{2}_{3}" + Write-Host "Using designation-specific Bounce Job buildId Template for EnvironmentType : $EnvironmentType" + $bounceJobBuildId = $bounceJobBuildIdTemplate -f $bounceJobBuildIdPrefix, $buildEnvironmentType, $buildEnvironmentName, $bounceJobBuildIdSuffix + } elseif ($EnvironmentType -in $environmentTypesWithSharedBounceJob) { + $bounceJobBuildIdTemplate = "{0}_{1}_{2}" + Write-Host "Using SHARED Bounce Job buildId Template for EnvironmentType : $EnvironmentType" + $bounceJobBuildId = $bounceJobBuildIdTemplate -f $bounceJobBuildIdPrefix, $buildEnvironmentType, $bounceJobBuildIdSuffix + + } else { + Write-Warning -Message "$logLead : Could not find buildId template for EnvironmentType : $EnvironmentType" + } + + return $bounceJobBuildId +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareAuthenticationHeaders.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareAuthenticationHeaders.ps1 new file mode 100644 index 0000000..0a9d480 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareAuthenticationHeaders.ps1 @@ -0,0 +1,34 @@ +function Get-CloudFlareAuthenticationHeaders { + +<# +.SYNOPSIS + Returns a Dictionary with the Headers Needed for CloudFlare API calls + +.DESCRIPTION + Returns a Dictionary containing the headers necessary to invoke a REST method in the CloudFlare API + User email is saved as X-Auth-Email and the user API key is saved as X-Auth-Key + +.PARAMETER cloudFlareEmail + [string] The email address of the user authorized to make the API call + +.PARAMETER cloudFlareAuthKey + [string] The API key of the user authorized to make the API call +#> + + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [Alias("X-Auth-Email")] + [string]$cloudFlareEmail, + + [Parameter(Mandatory=$true)] + [Alias("X-Auth-Key")] + [string]$cloudFlareAuthKey + ) + + $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" + $headers.Add("X-Auth-Email", $cloudflareEmail) + $headers.Add("X-Auth-Key", $cloudflareAuthKey) + + return $headers +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareZoneId.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareZoneId.ps1 new file mode 100644 index 0000000..d41f458 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-CloudFlareZoneId.ps1 @@ -0,0 +1,78 @@ +function Get-CloudFlareZoneId { +<# + +.SYNOPSIS + Retrieves the Zone ID for a Given Zone Name + +.DESCRIPTION + Makes an API call to CloudFlare to retrieve the zone ID, which is used in subsequent API calls + +.PARAMETER AuthenticationDictionary + Authentication headers required to execute requests. This object is the result from + calls to Get-CloudFlareAuthenticationHeaders + +.PARAMETER ZoneName + The name of the zone to search for + +.PARAMETER BaseUri + The base CloudFlare API URL. Defaults to https://api.cloudflare.com/client/v4 + +.LINK + +https://api.cloudflare.com/#zone-list-zones +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Alias("AuthenticationHeaders")] + [System.Collections.Generic.Dictionary[[String], [String]]]$AuthenticationDictionary, + + [Parameter(Mandatory = $true)] + [Alias("HostZone")] + [string]$ZoneName, + + [Parameter(Mandatory = $false)] + [Alias("CloudFlareBaseUri")] + [string]$BaseUri = "https://api.cloudflare.com/client/v4", + + [Parameter(Mandatory = $false)] + [Alias("ReturnActiveOnly")] + [bool]$ActiveOnly = $false + ) + + $logLead = Get-LogLeadName + + if ($ActiveOnly) { + $zoneLookupURL = "$BaseUri/zones?name=$ZoneName&status=active" + } else { + $zoneLookupURL = "$BaseUri/zones?name=$ZoneName" + } + + Write-Host "$logLead : Getting Zone Id from $zoneLookupURL" + $getZoneIdResult = Invoke-RestMethod -Method Get -ContentType "application/json" -Header $AuthenticationDictionary ` + -Uri "$zoneLookupURL" -ErrorAction SilentlyContinue + + if (!($getZoneIdResult.Success)) { + + $errorsToPrint = Test-IsNull $getZoneIdResult.Errors "Unknown Error" -Strict + Write-Warning "$logLead : An Error Occurred Making the API Call: $errorsToPrint" + return $null + } + + $resultDetails = $getZoneIdResult.result_info + $totalCount = $resultDetails.total_count + + if ($totalCount -eq 0) { + Write-Warning "$logLead : Found 0 Results for Zone $ZoneName" + return $null + } elseif ($totalCount -gt 1) { + Write-Warning "$logLead : Found $totalCount Results for Zone $ZoneName. Something is really wrong here." + return $null + } + + $zoneId = $getZoneIdResult.Result.Id + Write-Host "$logLead : Zone ID for Host $ZoneName is $zoneId" + + return $zoneId +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.Tests.ps1 new file mode 100644 index 0000000..cf8e3fb --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.Tests.ps1 @@ -0,0 +1,45 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-DataForSmokeTest" { + + Context "Master Connection String Parameter is Not Supplied" { + + It "Runs to Completion if the Server is an App Server" { + + Mock -ModuleName $moduleForMock Test-IsAppServer { return $true } + Mock -ModuleName $moduleForMock Invoke-QueryOnClientDatabase { } + Mock -ModuleName $moduleForMock Get-CatalogsFromMaster { } + + Get-DataForSmokeTest + Assert-MockCalled -ModuleName $moduleForMock Get-CatalogsFromMaster -Times 1 -Exactly + } + + It "Writes a Warning if the Server is Not an App Server" { + + Mock -ModuleName $moduleForMock Test-IsAppServer { return $false } + Mock -ModuleName $moduleForMock Get-CatalogsFromMaster { } + Mock -ModuleName $moduleForMock Invoke-QueryOnClientDatabase { } + + { (Get-DataForSmokeTest 3>&1) -match "This function can only be run on an app server or a connection string must be supplied" } | Should Be $true + } + } + + Context "Master Connection String Parameter is Supplied" { + + It "Uses the Supplied Master Connection String" { + + Mock -ModuleName $moduleForMock Test-IsAppServer { return $false } + Mock -ModuleName $moduleForMock Invoke-QueryOnClientDatabase { } + Mock -ModuleName $moduleForMock Get-CatalogsFromMaster {} -ParameterFilter { $connectionString -and $connectionString -eq "foo" } + + Get-DataForSmokeTest "foo" + Assert-MockCalled -ModuleName $moduleForMock Get-CatalogsFromMaster -Times 1 -Exactly + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.ps1 new file mode 100644 index 0000000..de55a45 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-DataForSmokeTest.ps1 @@ -0,0 +1,53 @@ +function Get-DataForSmokeTest { +<# +.SYNOPSIS + Gets the Data Necessary to Execute a Smoke Test +.NOTES + Must be run on an App Server or Have a Master Connection String Supplied +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("MasterConnectionString")] + [string]$connectionString + ) + + $logLead = (Get-LogLeadName); + + if (!(Test-IsAppServer) -and [String]::IsNullOrEmpty($connectionString)) { + Write-Warning ("$logLead : This function can only be run on an app server or a connection string must be supplied.") + return; + } + + $activeUserQuery = @" + DECLARE @bankEntityID bigint = (SELECT TOP 1 ID from core.Entity WHERE MasterUserID IS NULL AND TaxIdentificationNumber IS NULL) + + select top 1 u.UserIdentifier + FROM core.Users u + JOIN core.STSProviderUser spu on spu.UserID = u.ID + JOIN core.UserAccount ua on ua.UserID = u.ID + JOIN core.Account a on a.ID = ua.AccountID + JOIN core.UserWidget uw on uw.UserID = u.ID + WHERE (u.EntityID IS NULL OR u.EntityID <> @bankEntityID) + AND u.IsActive = 1 and ISNULL(u.IsLockedOut, 0) = 0 AND u.HasLoggedInBefore = 1 + AND u.LastLoginDate > DATEADD(month, -1, GETUTCDATE()) + AND ua.HideFromEndUser = 0 and ua.HiddenByEndUser = 0 AND ua.HasMasterRights = 1 AND ua.Deleted = 0 + AND a.Status = 0 + AND uw.DeactivationDate IS NULL + GROUP BY spu.STSID, u.UserIdentifier +"@ + + $masterCatalogs = Get-CatalogsFromMaster $connectionString + + [HashTable[]]$testValues = @() + foreach ($tenant in $masterCatalogs) { + Write-Host ("$logLead : Pullling Smoke Test Data for Tenant {0}" -f $tenant.Name) + $userIdentifier = Invoke-QueryOnClientDatabase $tenant $activeUserQuery + + $testValues += @{ Name = $tenant.Name; AdminSignature = $tenant.AdminSignature; ClientSignature = $tenant.Signature; UserIdentifier = $userIdentifier; } + } + + return $testValues +} + diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-EC2InstancesByHostname.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-EC2InstancesByHostname.ps1 new file mode 100644 index 0000000..0117661 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-EC2InstancesByHostname.ps1 @@ -0,0 +1,75 @@ +function Get-EC2InstancesByHostname { + <# + .SYNOPSIS + Returns EC2 instance objects by hostname. + + .PARAMETER Servers + The server list of hostnames to search for. + + .PARAMETER ProfileName + The AWS profile name to use when querying for EC2 instances. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + $Servers, + [Parameter(Mandatory=$true)] + $ProfileName + ) + + # Get a list of the currently supported AWS regions. + $supportedAwsRegions = (Get-SupportedAwsRegions) + + $results = @() + foreach($server in $Servers) { + $firstPeriodIndex = $server.IndexOf('.') + if ($firstPeriodIndex -eq -1) { + $firstPeriodIndex = $server.Length + } + # Strip the domain off of the hostname. That's how the hostname tag is stored. + $serverShort = $server.Substring(0, $firstPeriodIndex) + $serverShort = $serverShort.ToLower() + + # Figure out what region the server is in by subnet. + # Note: This is not a 100% guarantee to be correct, but should be correct most of the time. + $regionGuess = Get-AwsRegionByHostname -COMPUTERNAME $server + [array]$otherRegions = ($supportedAwsRegions | Where-Object { $_ -notlike $regionGuess } ) + [array]$regionsToSearch = @($regionGuess) + $otherRegions + + # Loop through each of the regions to search, and look for the instances we want. + # The Get-AwsRegionByHostname guess should be right most of the time, otherwise it will fall back to looping through other expected regions. + $instance = $null + foreach($region in $regionsToSearch) { + + # Query for the instances. + $filter = @{ + "alk:hostname" = $serverShort + } + [array]$instances = Get-InstancesByTag -Tags $filter -ProfileName $ProfileName -Region $region -IncludeOffline + + # Figure out if we found the instance. + $foundInstance = !(Test-IsCollectionNullOrEmpty $instances) + + # If we found an instance querying the EC2 API. + if($foundInstance) { + + # Sanity check. Make sure that we didn't find multiple servers with the same hostname. + if($instances.Count -gt 1) { + throw "Found multiple servers for hostname $server. Question your searching assumptions!" + } + + # Record the instance in the results and break the region loop. + $instance = $instances[0] + $results += $instance + break + } + } + + # Write an error if we could not find the instance in any of the expected regions. + if($null -eq $instance) { + Write-Error "Could not find EC2 instance for hostname $server" + continue + } + } + return $results +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-EntrustUserCredentialsFromSecretServer.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-EntrustUserCredentialsFromSecretServer.ps1 new file mode 100644 index 0000000..e8be1d6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-EntrustUserCredentialsFromSecretServer.ps1 @@ -0,0 +1,43 @@ +function Get-EntrustUserCredentialsFromSecretServer () { +<# +.SYNOPSIS + Gets the username and password of an Entrust user secret from Secret Server. + +.PARAMETER secretCredential + Credentials of the user to authenticate with on Secret Server. + +.PARAMETER environmentID + The environment ID of the Entrust user to retrieve (e.g. "12-2", "14") + +.PARAMETER entrustUserName + Entrust username on Secret Server you are searching for. + +.OUTPUTS + Either a credential object containing the username and password of the Entrust user or null. + +.EXAMPLE + Get-EntrustUserCredentialsFromSecretServer -secretCredential $credential -environmentID "17-0" -entrustUserName "Master1" + +Password Username +-------- -------- +SecretStuffs Master1 +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [PSCredential]$secretCredential, + [Parameter(Mandatory=$true)] + [String]$environmentID, + [Parameter(Mandatory=$true)] + [ValidateSet("Master1", "Master2", "Master3", "Web", "DB")] + [String]$entrustUserName + ) + + $loglead = (Get-LogLeadName) + + $folderName = "Entrust $($environmentID)" + $secretName = "$($folderName) $($entrustUserName) User" + + Write-Verbose "$loglead : Searching for secret '$secretName' in folder '$folderName'" + return ( Get-UserCredentialsFromSecretServer $secretCredential $folderName $secretName ) +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-EnvironmentShortName.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-EnvironmentShortName.ps1 new file mode 100644 index 0000000..229bb48 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-EnvironmentShortName.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Returns the shortened environment label given a full environment name. + +.PARAMETER EnvironmentName + Full environment name as generated by the Server List Parameter Builder Jobs: + https://ci.corp.alkamitech.com/project/Sre_ParameterBuilders_ServerLists +#> + +function Get-EnvironmentShortName { + [CmdletBinding()] + Param( + [string]$EnvironmentName + ) + + [Regex]$shortLabelRegex = "^(\b(Firehost|LT)\b)|(\b(Production|Staging|Dev|QA)\b)" + + $shortLabel = $shortLabelRegex.Replace($EnvironmentName, "").Trim().Replace(" ", " ") + Write-Host ("Short Environment Label: $shortLabel") + + return $shortLabel +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-LatestOrbAmi.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-LatestOrbAmi.ps1 new file mode 100644 index 0000000..1546140 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-LatestOrbAmi.ps1 @@ -0,0 +1,85 @@ +function Get-LatestOrbAmi() { +<# +.SYNOPSIS + Function to get the latest ORB Alkami AMI from AWS. + +.DESCRIPTION + Uses Name filters to get the AMI list and finds the latest one based on CreationDate property of the AMI. + Returns one Amazon.EC2.Model.Image object + +.PARAMETER profileName + [string] AWS Profile. ("Sandbox", "prod") + +.PARAMETER awsRegion + [string] AWS Region. ("us-east-1") + +.PARAMETER amiNameFilter + Filter string used to search for AMIs. Use this to override which AMI to search on. Accepts wildcard *. Not compatible with + $windowsServerVersion param unless the string has "__VERSION__" in it to accept the selection of $windowsServerVersion. + Current name filter is in the format "windows-__VERSION__-orb-base*" where "__VERSION__" is replaced by $windowsServerVersion + parameter, which is defaulted to "2016". + +.PARAMETER windowsServerVersion + String of the windows version to target. Currently, ValidationSet has "2016", "2019", and "2022" as options. + This will be used in the amiNameFilter param to get the AMI with the selected Windows version. + +.EXAMPLE + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 -AmiNameFilter "windows-2016-orb-base*" + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 -AmiNameFilter "windows-__VERSION__-orb-base*" -WindowsServerVersion "2019" + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 -WindowsServerVersion "2016" + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 -WindowsServerVersion "2019" + Get-LatestOrbAMI -ProfileName temp-dev -Region us-west-2 -WindowsServerVersion "2022" +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [string]$profileName, + + [Alias("Region")] + [Parameter(Mandatory=$true)] + [string]$awsRegion, + + [Parameter(Mandatory=$false)] + [string]$amiNameFilter = "windows-__VERSION__-orb-base*", + + [Parameter(Mandatory=$false)] + [ValidateSet("2016", "2019", "2022")] + [string]$windowsServerVersion = "2016" + ) + $logLead = (Get-LogLeadName) + + Import-AWSModule # EC2 + + # If $windowsServerVersion is set and $amiNameFilter has a placeholder, use it in the filter. + $amiNameFilter = $amiNameFilter.Replace("__VERSION__", $windowsServerVersion) + + # Filter searching + $filters = @() + $nameValues = New-Object 'collections.generic.list[string]' + $nameValues.add($amiNameFilter) + # Filter by AMI Name property. + $filterNames = New-Object Amazon.EC2.Model.Filter -Property @{Name = "name"; Values = $nameValues} + # This gets private images shared with the account in $profileName. + $filterVisibility = New-Object Amazon.EC2.Model.Filter -Property @{Name = "is-public"; Values = "false"} + # This gets only available AMIs, not pending, not failed. + $filterState = New-Object Amazon.EC2.Model.Filter -Property @{Name = "state"; Values = "available"} + + $filters += $filterNames + $filters += $filterVisibility + $filters += $filterState + + # Get all of the AMI Images by tags defined above. Returns a collection of Amazon.EC2.Model.Image objects. + $EC2ImageList = ( + Invoke-CommandWithRetry -Arguments ($profileName, $awsRegion, $filters) -MaxRetries 3 -Exponential -ScriptBlock { + param($sbProfileName, $sbAwsRegion, $sbFilters) + return Get-EC2Image -ProfileName $sbProfileName -Region $sbAwsRegion -Filter $sbFilters + } + ) + + # Find the latest AMI by CreateDate + $latestImage = $EC2ImageList | Sort-Object CreationDate | Select-Object -Last 1 + Write-Host "$logLead : Latest ORB AMI $($latestImage.ImageId) CreationDate: $($latestImage.CreationDate)" + + return $latestImage +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.ps1 new file mode 100644 index 0000000..c8447fb --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.ps1 @@ -0,0 +1,73 @@ +function Get-LoadBalancerState { + <# + .SYNOPSIS + Gets the current state of a server with regards to a load balancer. + It will check both Nginx and ALB/NLB/ELBs + Supports Web/App servers. Mic/Fab servers are no-ops. + + .PARAMETER Server + The string to get the server type string from. Expects the fully qualified hostname (blah.fh.local) + + .PARAMETER AwsProfileName + The AWS Profile the Server is in. + + .PARAMETER AwsRegion + The AWS Region the Server is in. + + .OUTPUTS + The state of the host with regards to the loadbalancer. Returns Active/Standby to match the output of Get-ASInstanceState. + This doesn't imply that it's healthy, just that it has traffic directed to it. + + .NOTES + This function will not succeed if run directly on Production and Staging web servers. They do not have the necessary IAM permissions to + get the NGINX state. Run this remotely (ie: from an agent) and target those servers instead. + + #> + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [string]$Server, + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + [Parameter(Mandatory = $false)] + [string]$AwsRegion + ) + + $logLead = Get-LogLeadName + + # Get the string type of the server. + $serverType = Get-ServerTypeByHostname -ComputerName $Server + + # Early out if it's a mic or fab server. These don't use load balancers. + if (($serverType -eq "Mic") -or ($serverType -eq "Fab")) { + Write-Host "$logLead : [$serverType] servers do not use a load balancer. Exiting." + return + } + + # Throw if this is an unsupported/unknown server type. If it's not a Web or App. + if (!(($serverType -eq "Web") -or ($serverType -eq "App"))) { + throw "$logLead : getting a load balancer on server type [$serverType] for host [$Server] is unsupported." + } + + # Determine what type of load balancer the environment uses. + # Only Staging/Production web servers use nginx, and everything else is AWS load balancers. + $ec2Instance = @(Get-EC2InstancesByHostname -Servers $Server -ProfileName $AwsProfileName)[0] + + # Should only get one instance for a given Instance Id. Get the environment value from the tag. + $environmentType = $ec2Instance.Tag.Where({ $_.Key -eq "alk:env" }).Value.ToLower() + + $useNginx = $serverType -eq "Web" -and (($environmentType -eq "staging") -or ($environmentType -eq "prod")) + $loadBalancerName = if ($useNginx) { "NGINX" } else { "AWS" } + Write-Host "$logLead : Determined that server uses an [$loadBalancerName] load balancer." + + if ($useNginx) { + # Handle nginx load balancers. + $state = Get-NginxHostStates -Hostname $Server -TargetEnvironment $environmentType -AwsProfileName $AwsProfileName -AwsRegion $AwsRegion + } else { + # Handle AWS load balancers. + $state = Get-ASInstanceState -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + } + Write-Host "$logLead : Returning State of [$state]" + return $state +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.tests.ps1 new file mode 100644 index 0000000..8c03401 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerState.tests.ps1 @@ -0,0 +1,101 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-LoadBalancerState" { + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { } + Context "When called for a AWS staging app server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'staging' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, returns active" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + $result = Get-LoadBalancerState -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be "Active" + } + } + Context "When called for a AWS prod app server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'prod' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, returns active" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + $result = Get-LoadBalancerState -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be "Active" + } + } + Context "When called for a Nginx Staging Web server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'staging' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerState -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be "Active" + } + } + Context "When called for a Nginx prod Web server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'prod' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerState -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be "Active" + } + } + Context "When called with an incorrect server type" { + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { } + + It "returns null for wrong type" { + $result = Get-LoadBalancerState -Server "MIC111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be $null + } + } + Context "When called with an aws dev server type" { + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'dev' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "returns null for wrong env" { + $result = Get-LoadBalancerState -Server "web111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be $null + } + } + Context "When called with an aws qa server type" { + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'qa' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "returns null for wrong env" { + $result = Get-LoadBalancerState -Server "web111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.ps1 new file mode 100644 index 0000000..4206af2 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.ps1 @@ -0,0 +1,89 @@ +function Get-LoadBalancerStateAndHealth { + <# + .SYNOPSIS + Gets the current state of a server with regards to a load balancer, and will return the health of the server + It will check both Nginx and ALB/NLB/ELBs + Supports Web/App servers. Mic/Fab servers are no-ops. + + .PARAMETER Server + The string to get the server type string from. Expects the fully qualified hostname (blah.fh.local) + + .PARAMETER AwsProfileName + The AWS Profile the Server is in. + + .PARAMETER AwsRegion + The AWS Region the Server is in. + + .OUTPUTS + The state of the host with regards to the loadbalancer and the health status. Normalizes the strings to match the output of Get-ASInstanceState. + + .NOTES + This function will not succeed if run directly on Production and Staging web servers. They do not have the necessary IAM permissions to + get the NGINX state. Run this remotely (ie: from an agent) and target those servers instead. + + #> + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [string]$Server, + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + [Parameter(Mandatory = $false)] + [string]$AwsRegion + ) + + $logLead = Get-LogLeadName + + # Get the string type of the server. + $serverType = Get-ServerTypeByHostname -ComputerName $Server + + # Early out if it's a mic or fab server. These don't use load balancers. + if (($serverType -eq "Mic") -or ($serverType -eq "Fab")) { + Write-Host "$logLead : [$serverType] servers do not use a load balancer. Exiting." + return + } + + # Throw if this is an unsupported/unknown server type. If it's not a Web or App. + if (!(($serverType -eq "Web") -or ($serverType -eq "App"))) { + throw "$logLead : getting a load balancer on server type [$serverType] for host [$Server] is unsupported." + } + + # Determine what type of load balancer the environment uses. + # Only Staging/Production web servers use nginx, and everything else is AWS load balancers. + $ec2Instance = @(Get-EC2InstancesByHostname -Servers $Server -ProfileName $AwsProfileName)[0] + + # Should only get one instance for a given Instance Id. Get the environment value from the tag. + $environmentType = $ec2Instance.Tag.Where({ $_.Key -eq "alk:env" }).Value.ToLower() + + $useNginx = $serverType -eq "Web" -and (($environmentType -eq "staging") -or ($environmentType -eq "prod")) + $loadBalancerName = if ($useNginx) { "NGINX" } else { "AWS" } + Write-Host "$logLead : Determined that server uses an [$loadBalancerName] load balancer." + + $health = $null + $state = $null + if ($useNginx) { + # Handle nginx load balancers. + $state = Get-NginxHostStates -Hostname $Server -TargetEnvironment $environmentType -AwsProfileName $AwsProfileName -AwsRegion $AwsRegion -TreatNGINXUnhealthyAsUnhealthy + if ($state -eq "Unhealthy") { + # In nginx land, just because a server is temporarly unhealthy, does not mean it is OUT of the LB + $health = $state.ToUpper() + $state = ConvertTo-NormalizedLoadBalancerState NGINX -InputState $state -TreatNGINXUnhealthyAsUnhealthy:$false + } else { + # In nginx land, if a server is not unhealthy, it is healthy + $health = "HEALTHY" + } + } else { + # Handle AWS load balancers. + $state = Get-ASInstanceState -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + $health = Get-ASInstanceHealth -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + } + # Return combined state and health + $returnObject = @{ + Health = $health + State = $state + } + + Write-Host "$logLead : Returning State: [$($returnObject.State)], Health [$($returnObject.Health)]" + return $returnObject +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.tests.ps1 new file mode 100644 index 0000000..c0ccc59 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-LoadBalancerStateAndHealth.tests.ps1 @@ -0,0 +1,152 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-LoadBalancerStateAndHealth" { + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { } + Context "When called for a AWS staging app server type" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + Mock -CommandName Get-ASInstanceHealth -ModuleName $moduleForMock -MockWith { return "HEALTHY" } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'staging' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, state returns active" { + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If Healthy, health returns healthy" { + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "HEALTHY" + } + } + Context "When called for a AWS prod app server type" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + Mock -CommandName Get-ASInstanceHealth -ModuleName $moduleForMock -MockWith { return "UNHEALTHY" } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'prod' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, State returns active" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If UNHEALTHY, health returns UNHEALTHY" { + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "UNHEALTHY" + } + } + Context "When called for a Nginx Staging Web server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'staging' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, state returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If unhealthy, state returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Unhealthy" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If unhealthy, health returns unhealthy" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Unhealthy" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If active, health returns healthy" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "healthy" + } + } + Context "When called for a Nginx prod Web server type" { + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'prod' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, state returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If unhealthy, state returns active" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Unhealthy" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If unhealthy, health returns unhealthy" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Unhealthy" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If active, health returns healthy" { + Mock -CommandName Get-NginxHostStates -ModuleName $moduleForMock -MockWith { return "Active" } + $result = Get-LoadBalancerStateAndHealth -Server "WEB111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "healthy" + } + } + Context "When called with an incorrect server type" { + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { } + + It "returns null for wrong type" { + $result = Get-LoadBalancerStateAndHealth -Server "MIC111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result | Should -Be $null + } + } + Context "When called with an aws dev server type" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + Mock -CommandName Get-ASInstanceHealth -ModuleName $moduleForMock -MockWith { return "UNHEALTHY" } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'dev' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, State returns active" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If UNHEALTHY, health returns UNHEALTHY" { + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "UNHEALTHY" + } + } + Context "When called with an aws qa server type" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + Mock -CommandName Get-ASInstanceHealth -ModuleName $moduleForMock -MockWith { return "UNHEALTHY" } + Mock -CommandName Get-EC2InstancesByHostname -ModuleName $moduleForMock -MockWith { + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add([pscustomobject]@{Key = 'alk:env'; Value = 'qa' }) + $ec2Instance = [pscustomobject]@{Tag = $testTags } + return $ec2Instance + } + It "If active, State returns active" { + Mock -CommandName Get-ASInstanceState -ModuleName $moduleForMock -MockWith { return "active" } + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.State | Should -Be "Active" + } + It "If UNHEALTHY, health returns UNHEALTHY" { + $result = Get-LoadBalancerStateAndHealth -Server "APP111111" -AwsProfileName "temp-fake" -AwsRegion "not-us-east-1" + $result.Health | Should -Be "UNHEALTHY" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxAuthHeaders.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxAuthHeaders.ps1 new file mode 100644 index 0000000..371eb59 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxAuthHeaders.ps1 @@ -0,0 +1,62 @@ +function Get-NginxAuthHeaders { + +<# + +.SYNOPSIS + Builds the NGINX authorization headers from the specified SSM parameters + +.PARAMETER UserNameSSMPath + [string] The SSM paramter name for the username. + +.PARAMETER PasswordSSMPath + [string] The SSM paramter name for the password. + +.PARAMETER ProfileName + [string] Specific AWS CLI Profile to use in AWS API calls. Unused if not specified. + +.PARAMETER Region + [string] Specific AWS CLI Region to use in AWS API calls. Unused if not specified. + +#> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$UserNameSSMPath = "/teamcity/nginx-staging-username", + + [Parameter(Mandatory = $false)] + [string]$PasswordSSMPath = "/teamcity/nginx-staging-password", + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule # SSM + + $splatParams = @{} + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $splatParams["ProfileName"] = "$ProfileName" + } + + if (!([string]::IsNullOrEmpty($Region))) { + $splatParams["Region"] = "$Region" + } + + Write-Verbose "$logLead : Getting secrets from SSM" + $Username = (Get-SSMParameter -Name $UserNameSSMPath @splatParams).Value + $Password = (Get-SSMParameter -Name $PasswordSSMPath -WithDecryption $true @splatParams).Value + Write-Verbose "$logLead : Done getting secrets from SSM" + + if ($username -and $password) { + $GetAuthHeaders = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($username):$($password)")) } + } else { + Write-Error "$logLead : Unable to retrieve username or password" + } + return $GetAuthHeaders +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.ps1 new file mode 100644 index 0000000..6e75fda --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.ps1 @@ -0,0 +1,130 @@ +function Get-NginxHostStates { + <# + .Synopsis + Returns the current state of a host the specified environment + + .Parameter targetEnvironment + Required. Must be "staging" or "prod" + + .Parameter hostname + Requird. Hostname to toggle "webxxxxx" + + .PARAMETER AwsProfileName + [string] Specific AWS CLI Profile to use in AWS API calls. + + .PARAMETER AwsRegion + [string] Specific AWS CLI Region to use in AWS API calls. + + .PARAMETER TreatNGINXUnhealthyAsUnhealthy + [switch] Should the server state unhealthy be returned as active or unhealthy? + + .OUTPUTS + Return a normalized status (active/standby) if the server exists in NGINX + Returns an empty string if the server does not exist in NGINX + #> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost", + + [Parameter(Mandatory = $false)] + [ValidateSet("staging","prod")] + [string]$TargetEnvironment = $null, + + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + + [Parameter(Mandatory = $false)] + [string]$AwsRegion, + + [Parameter(Mandatory = $false)] + [switch]$TreatNGINXUnhealthyAsUnhealthy = $false + ) + + $logLead = Get-LogLeadName + + # attempt to resolve host to toggle + #TODO: This should be changed to something mockable. Even just a wrapper. + # Currently, our tests rely on both resolving AND pinging "google.com" + # Created SRE-13329 + + if (Compare-StringToLocalMachineIdentifiers $Hostname) { + $alkHostIP = (Get-IpAddress); + } else { + $alkHostIP = @(Get-IPAddressesForName $Hostname) + if ($null -eq $alkHostIP[0] ) { + Write-Error "$logLead : unable to resolve IP address of $Hostname" + throw + } + } + + # If the target environment isn't specified, construct it from the alk:env tag in the environment. + if ([string]::IsNullOrWhiteSpace($TargetEnvironment)) { + $tags = (invoke-command -ComputerName $Hostname -ScriptBlock { Get-CurrentInstanceTags }); + foreach ($tag in $tags) { + if($tag.Key -eq "alk:env"){ + $TargetEnvironment = $tag.Value + } + } + } + + # Get Nginx server IP addresses + $nginxIpAddresses = Get-NginxIpAddresses -TargetEnvironment $TargetEnvironment -AwsProfileName $AwsProfileName -AwsRegion $AwsRegion + if (Test-IsCollectionNullOrEmpty -collection $nginxIpAddresses) { + Write-Error "$logLead : no Nginx Servers found" + throw + } + + Write-Verbose "nginx servers $nginxIpAddresses" + + # Pre-flight the NGINX server connectivity. We don't want to proceed unless they are all available. + foreach ($nginxIpAddress in $nginxIpAddresses) { + $tcpResult = Test-TcpConnection -ipAddress $nginxIpAddress -port 8080 -msTimeout 5000 + if ($false -eq $tcpResult) { + # If TCP test fails, we do not want to proceed. NGINX servers may become out of sync + Write-Error "$logLead : This NGINX server was unreachable $nginxIpAddress" + throw + } + } + + foreach ($nginxIpAddress in $nginxIpAddresses) { + + Write-Verbose "Checking $nginxIpAddress" + # gets the current state + $getResults = Get-NginxUpstreams -NginxServer $nginxIpAddress -targetEnvironment $TargetEnvironment -ProfileName $AwsProfileName -Region $AwsRegion + + # If getting the upstreams fails, but ping succeded, nginx may become out of sync + if (!$getResults) { + Write-Error "$logLead : Getting upstreams failed." + break + } + + $resultsHash = $getResults | ConvertTo-Json -Depth 4 | ConvertFrom-JsonToHashtable + foreach ($serviceName in $resultsHash.Keys) { + Write-Verbose "$logLead : Looking for $Hostname in ServiceName : $serviceName" + foreach ($peer in $resultsHash["$serviceName"]["peers"]) { + Write-Verbose "$logLead : Looking for $Hostname in Peer : $($peer.name)" + + Write-Verbose "$logLead : Testing if $($peer["server"]) matches '$($alkHostIP):*'" + #The $($varName) format is necessary to put a ":" after a variable, otherwise it thinks it's a scope variable + #Like $Global:varname + #Alternate matcher: '-match "^$($alkHostIp):"' + if ($peer["server"] -like "$($alkHostIp):*") { + $peerState = $peer["state"] + Write-Verbose "Nginx Server: $serviceName" + Write-Verbose "State: $peerState" + if ($null -eq $returnState) { + $returnState = $peerState + } elseif ($returnState -ne $peerState) { + # This variable is used in Install_Packages as a string match + $magicString = "$logLead : Got differing states for the same host from different NGINX servers. Investigate." + Write-Error $magicString + } + } + } + } + } + $normalizedReturnState = ConvertTo-NormalizedLoadBalancerState -LBType "NGINX" -InputState $returnState -TreatNGINXUnhealthyAsUnhealthy:$TreatNGINXUnhealthyAsUnhealthy + return $normalizedReturnState +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.tests.ps1 new file mode 100644 index 0000000..970ddc7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxHostStates.tests.ps1 @@ -0,0 +1,294 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NginxHostStates" { + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith { } + $upstreamJson = @" + { + "lane-fake-ssl": { + "peers": [ + { + "id": 0, + "server": "10.10.10.10:443", + "name": "10.10.10.10:443", + "backup": false, + "weight": 1, + "state": "up", + "active": 0, + "requests": 300, + "header_time": 200, + "response_time": 200, + "responses": { + "1xx": 0, + "2xx": 300, + "3xx": 30, + "4xx": 1, + "5xx": 0, + "total": 331 + }, + "sent": 700000, + "received": 4000000, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0, + "selected": "2021-02-25T23:06:38Z" + } + ], + "keepalive": 0, + "zombies": 0, + "zone": "lane-fake-ssl" + } + } +"@ + $unhealthyUpstreamJson = @" +{ + "lane-fake-ssl": { + "peers": [ + { + "id": 0, + "server": "10.10.10.10:443", + "name": "10.10.10.10:443", + "backup": false, + "weight": 1, + "state": "Unhealthy", + "active": 0, + "requests": 300, + "header_time": 200, + "response_time": 200, + "responses": { + "1xx": 0, + "2xx": 300, + "3xx": 30, + "4xx": 1, + "5xx": 0, + "total": 331 + }, + "sent": 700000, + "received": 4000000, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0, + "selected": "2021-02-25T23:06:38Z" + } + ], + "keepalive": 0, + "zombies": 0, + "zone": "lane-fake-ssl" + } +} +"@ + $updownstreamJson = @" + { + "lane-fake-ssl": { + "peers": [ + { + "id": 0, + "server": "10.10.10.10:443", + "name": "10.10.10.10:443", + "backup": false, + "weight": 1, + "state": "up", + "active": 0, + "requests": 300, + "header_time": 200, + "response_time": 200, + "responses": { + "1xx": 0, + "2xx": 300, + "3xx": 30, + "4xx": 1, + "5xx": 0, + "total": 331 + }, + "sent": 700000, + "received": 4000000, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0, + "selected": "2021-02-25T23:06:38Z" + }, + { + "id": 1, + "server": "10.10.10.10:443", + "name": "10.10.10.10:443", + "backup": false, + "weight": 1, + "state": "down", + "active": 0, + "requests": 300, + "header_time": 200, + "response_time": 200, + "responses": { + "1xx": 0, + "2xx": 300, + "3xx": 30, + "4xx": 1, + "5xx": 0, + "total": 331 + }, + "sent": 700000, + "received": 4000000, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0, + "selected": "2021-02-25T23:06:38Z" + } + ], + "keepalive": 0, + "zombies": 0, + "zone": "lane-fake-ssl" + } + } +"@ + + Context "When Provided with an un-findable IP address" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { } + + It "Writes An Error" { + { Get-NginxHostStates -Hostname "255.255.255.255" -TargetEnvironment "staging" } | Should -Throw + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "unable to resolve IP address" } + } + } + + Context "When Nginx Servers Cannot Be Found" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { return } + + It "Writes An Error" { + { Get-NginxHostStates -Hostname "255.255.255.255" -TargetEnvironment "staging" } | Should -Throw + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "no Nginx Servers found" } + } + } + + Context "When Nginx Servers Cannot Be Reached" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { return } + + It "Writes An Error" { + { Get-NginxHostStates -Hostname "255.255.255.255" -TargetEnvironment "staging" } | Should -Throw + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "This NGINX server was unreachable" } + } + } + + Context "When there are no Nginx upstreams" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { return } + Mock -CommandName ConvertTo-NormalizedLoadBalancerState -ModuleName $moduleForMock -MockWith { return "Active" } + + It "Writes An Error" { + Get-NginxHostStates -TargetEnvironment "staging" + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Getting upstreams failed." } + } + } + + Context "When called with a happy path" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { return $upstreamJson | ConvertFrom-JsonToHashtable } + + It "Returns State" { + $result = Get-NginxHostStates -TargetEnvironment "staging" + + $result | Should -Be "Active" + } + } + + Context "When NGINX servers have different states for the same host" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { return $updownstreamJson | ConvertFrom-JsonToHashtable } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { return } + + It "Writes an error" { + Get-NginxHostStates -TargetEnvironment "staging" + + # Look for the magic string + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match " : Got differing states for the same host from different NGINX servers. Investigate." } + } + } + Context "When called with TreatNGINXUnhealthyAsUnhealthy when unhealthy" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { return $unhealthyUpstreamJson | ConvertFrom-JsonToHashtable } + + It "Returns State" { + $result = Get-NginxHostStates -TargetEnvironment "staging" -TreatNGINXUnhealthyAsUnhealthy + + $result | Should -Be "Unhealthy" + } + } + Context "When called without TreatNGINXUnhealthyAsUnhealthy when unhealthy" { + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return "localhost" } + Mock -CommandName Get-IpAddress -ModuleName $moduleForMock -MockWith { return "10.10.10.10" } + Mock -CommandName Get-IPAddressesForName -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-NginxIpAddresses -ModuleName $moduleForMock -MockWith { return @("1.1.1.1") } + Mock -CommandName Test-TcpConnection -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Get-NginxUpstreams -ModuleName $moduleForMock -MockWith { return $unhealthyUpstreamJson | ConvertFrom-JsonToHashtable } + + It "Returns State" { + $result = Get-NginxHostStates -TargetEnvironment "staging" + + $result | Should -Be "Active" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.ps1 new file mode 100644 index 0000000..7dd3ca4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.ps1 @@ -0,0 +1,106 @@ +function Get-NginxIpAddresses { + +<# + +.SYNOPSIS + Retrieves the NGINX server IP addresses for a given environment. + +.DESCRIPTION + Retrieves the NGINX server IP addresses for a given environment. Currently only supports Production + and Staging since no other environments utilize NGINX. + +.PARAMETER TargetEnvironment + [string] The target environment for the NGINX servers. Must be prod or staging. + +.PARAMETER AwsProfileName + [string] Specific AWS CLI Profile to use in AWS API calls. Unused if not specified. + +.PARAMETER AwsRegion + [string] Specific AWS CLI Region to use in AWS API calls. Unused if not specified. + +.EXAMPLE + Get-NginxIpAddresses -TargetEnvironment staging -AwsProfileName Prod + +10.19.200.89 +10.19.201.76 + +.INPUTS + None +#> + + [CmdletBinding()] + + Param( + [Parameter(Mandatory = $true)] + [ValidateSet("prod", "staging", IgnoreCase = $true)] + [string]$TargetEnvironment, + + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + + [Parameter(Mandatory = $false)] + [string]$AwsRegion + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule # ELBv2 and EC2 + + $actualTargetEnvironment = $TargetEnvironment.ToLowerInvariant() + + # These are fixed values to match actual implementation. + $prodNginxTargetGroupName = 'Prod-NginxTrusted' + $stagingNginxTargetGroupName = 'Stage-NginxTrusted' + + $splatParams = @{} + + if (!([string]::IsNullOrEmpty($AwsProfileName))) { + $splatParams["ProfileName"] = "$AwsProfileName" + } + + if (!([string]::IsNullOrEmpty($AwsRegion))) { + $splatParams["Region"] = "$AwsRegion" + } + + $targetGroupName = switch ( $actualTargetEnvironment ) { + 'prod' { $prodNginxTargetGroupName } + 'staging' { $stagingNginxTargetGroupName } + } + Write-Verbose "$logLead : Computed ELB target group name as '$targetGroupName' for NGINX in $TargetEnvironment" + + #SRE-17377 - CommandWithRetry to fix 429 'Rate Exceeded' error + $elbScript = { + param($sb_targetGroupName, $sb_splatParams) + + $returnGroup = (Get-ELB2TargetGroup -Name $sb_targetGroupName @sb_splatParams) + return $returnGroup + } + + $targetGroup = Invoke-CommandWithRetry -Arguments ($targetGroupName, $splatParams) -Seconds 2.5 -ScriptBlock $elbScript + + if (Test-IsCollectionNullOrEmpty -collection $targetGroup) { + + Write-Error "$logLead : No ELB target group named '$targetGroupName' found using profile '$AwsProfileName' in region '$AwsRegion'" + return $null + + } elseif( $targetGroup.Count -gt 1 ) { + + Write-Error "$logLead : ELB target group search returned more than one result." + return $null + } + + $targetGroupArn = $targetGroup.TargetGroupArn + Write-Verbose "$logLead : Target group ARN is '$targetGroupArn'" + + $targetGroupMembers = Get-ELB2TargetHealth -TargetGroupArn $targetGroupArn @splatParams + if (Test-IsCollectionNullOrEmpty -collection $targetGroupMembers) { + + Write-Error "$logLead : No instances found in target group '$targetGroupName'" + return $null + } + + $nginxInstanceIds = $targetGroupMembers.Target.Id + Write-Verbose "$logLead : Found $($nginxInstanceIds.Count) instances associated with '$targetGroupArn'" + + return ( Get-EC2Instance -InstanceId $nginxInstanceIds @splatParams ).Instances.PrivateIpAddress +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.tests.ps1 new file mode 100644 index 0000000..75500f4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxIpAddresses.tests.ps1 @@ -0,0 +1,150 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NginxIpAddresses" { + + Context "Target Environment Should Not Be Null" { + + It "Parameter Validation Should Throw" { + { Get-NginxIpAddresses -TargetEnvironment $null } | Should Throw + } + } + + Context "Target Environment Should Not Be Empty" { + + It "Parameter Validation Should Throw" { + { Get-NginxIpAddresses -TargetEnvironment "" } | Should Throw + } + } + + Context "Target Environment Should Be In Validation Set" { + + It "Parameter Validation Should Throw" { + { Get-NginxIpAddresses -TargetEnvironment "Qa" } | Should Throw + } + } + + Context "Target Environment Is Case Insensitive" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return @{ 'TargetGroupArn' = 'Test' } } + Mock -CommandName Get-ELB2TargetHealth -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Get-EC2Instance -ModuleName $moduleForMock -MockWith { return @() } + + It "Parameter Validation Should Not Throw" { + { Get-NginxIpAddresses -TargetEnvironment "PROD" } | Should Not Throw + } + } + + Context "Writes Error if AWS Target Group Is Null" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return $null } + + $result = ( Get-NginxIpAddresses -TargetEnvironment "prod" ) + + It "Called Get-ELB2TargetGroup" { + Assert-MockCalled -CommandName Get-ELB2TargetGroup -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + + It "Called Write-Error With an Appropriate Error Message" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "No ELB target group" } + } + + It "Returned Null" { + $result | Should -BeNull + } + } + + Context "Writes Error if AWS Target Group Is Empty" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return @() } + + $result = ( Get-NginxIpAddresses -TargetEnvironment "prod" ) + + It "Called Get-ELB2TargetGroup" { + Assert-MockCalled -CommandName Get-ELB2TargetGroup -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + + It "Called Write-Error With an Appropriate Error Message" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "No ELB target group" } + } + + It "Returned Null" { + $result | Should -BeNull + } + } + + Context "Writes Error if AWS Target Group Returns More Than One Result" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return @("Test", "Test") } + + $result = ( Get-NginxIpAddresses -TargetEnvironment "prod" ) + + It "Called Get-ELB2TargetGroup" { + Assert-MockCalled -CommandName Get-ELB2TargetGroup -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + + It "Called Write-Error With an Appropriate Error Message" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "search returned more than one result." } + } + + It "Returned Null" { + $result | Should -BeNull + } + } + + Context "Writes Error if AWS Target Group Instance List Is Null" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return @{ 'TargetGroupArn' = 'Test' } } + Mock -CommandName Get-ELB2TargetHealth -ModuleName $moduleForMock -MockWith { return $null } + + $result = ( Get-NginxIpAddresses -TargetEnvironment "prod" ) + + It "Called Get-ELB2TargetGroup" { + Assert-MockCalled -CommandName Get-ELB2TargetHealth -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + + It "Called Write-Error With an Appropriate Error Message" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "No instances found" } + } + + It "Returned Null" { + $result | Should -BeNull + } + } + + Context "Writes Error if AWS Target Group Instance List Is Empty" { + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ELB2TargetGroup -ModuleName $moduleForMock -MockWith { return @{ 'TargetGroupArn' = 'Test' } } + Mock -CommandName Get-ELB2TargetHealth -ModuleName $moduleForMock -MockWith { return @() } + + $result = ( Get-NginxIpAddresses -TargetEnvironment "prod" ) + + It "Called Get-ELB2TargetGroup" { + Assert-MockCalled -CommandName Get-ELB2TargetHealth -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock + } + + It "Called Write-Error With an Appropriate Error Message" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "No instances found" } + } + + It "Returned Null" { + $result | Should -BeNull + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-NginxUpstreams.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-NginxUpstreams.ps1 new file mode 100644 index 0000000..9e181fe --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-NginxUpstreams.ps1 @@ -0,0 +1,59 @@ +function Get-NginxUpstreams { + <# +.SYNOPSIS + Get the upstreams from NGINX via HTTP API for a given environment and Nginx server + +.PARAMETER NginxServer + The domain name part of the API URI to query + +.PARAMETER Port + The port part of the API URI to query + +.PARAMETER UpstreamAPIPartialPath + The path part of the API URI to query + +.PARAMETER TargetEnvironment + Which Environment to query; staging or prod + +.PARAMETER ProfileName + AWS ProfileName + +.PARAMETER Region + AWS Region + +#> + [CmdletBinding()] + param ( + [Parameter(mandatory)] + [string]$NginxServer, + + [string]$Port = "8080", + + [string]$UpstreamAPIPartialPath = "/api/3/http/upstreams/", + + [Parameter(Mandatory = $true)] + [ValidateSet("staging", "prod")] + [string]$TargetEnvironment, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + ) + + $logLead = Get-LogLeadName + + if ($TargetEnvironment -eq "staging") { + $getAuthHeaders = Get-NginxAuthHeaders -ProfileName $ProfileName -Region $Region + } + + $uri = "$($NginxServer):$($Port)$($UpstreamAPIPartialPath)" + Write-Host "$logLead : Getting upstreams from $uri" + $results = Invoke-RestMethod -Uri $uri -Headers $getAuthHeaders -Method 'GET' -UseBasicParsing + + if ($null -eq $results){ + Write-Error "$logLead : No upsteams found at $uri" + } + return $results +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-OrbAutoScalingGroup.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-OrbAutoScalingGroup.ps1 new file mode 100644 index 0000000..ee53c77 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-OrbAutoScalingGroup.ps1 @@ -0,0 +1,61 @@ +function Get-OrbAutoScalingGroup { +<# +.SYNOPSIS + Gets all of the Auto Scaling Groups that are ORB-specific ASGs. + +.DESCRIPTION + Will return ORB-specific ASGs using the following tag filters: + alk:service = orb + + Returns a System.Collections.Generic.List of Amazon.AutoScaling.Model.AutoScalingGroup objects. + +.PARAMETER profileName + [string] AWS Profile. ("Sandbox", "prod") + +.PARAMETER awsRegion + [string] AWS Region. ("us-east-1") + +.EXAMPLE + Get-OrbAutoScalingGroup -profileName Sandbox -Region us-east-1 +#> + [CmdletBinding()] + [OutputType([System.Collections.Generic.List[Amazon.AutoScaling.Model.AutoScalingGroup]])] + Param ( + [Parameter(Mandatory=$true)] + [string]$profileName, + + [Alias("Region")] + [Parameter(Mandatory=$true)] + [string]$awsRegion + ) + $logLead = (Get-LogLeadName) + + Import-AWSModule # AS + + # Get all of the AutoScalingGroups in AWS. + Write-Host "$logLead : Getting all ORB ASGs." + + $asgList = ( + Invoke-CommandWithRetry -Arguments ($profileName, $awsRegion) -MaxRetries 3 -Exponential -ScriptBlock { + param($sbProfileName, $sbAwsRegion) + return Get-ASAutoScalingGroup -ProfileName $sbProfileName -Region $sbAwsRegion + } + ) + + # Loop over the groups and filter out the asgs we don't need. + $filteredAsgList = [System.Collections.Generic.List[Amazon.AutoScaling.Model.AutoScalingGroup]]::new() + + foreach($asg in $asgList){ + # Loop to find all asgs with alk:service tags... + foreach($tag in $asg.Tags) { + if($tag.Key -eq "alk:service" -and $tag.Value -eq "orb") { + # Add this item to the main filtered list as it has tags we need. + $filteredAsgList.Add($asg) + Write-Verbose "$logLead : Found an ASG that matches: $($asg.AutoScalingGroupName)." + break + } + } + } + + return $filteredAsgList +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.ps1 new file mode 100644 index 0000000..061c6a3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.ps1 @@ -0,0 +1,47 @@ +function Get-PercentageServerCount { +<# +.SYNOPSIS + Returns a number of servers given a percentage of servers, by server list or count. + Any non-zero percentage will return at least 1 server. + +.PARAMETER ServerCount + Count of servers to consider to determine a percentage of servers. + +.PARAMETER Percentage + [0-100] Percentage of servers to return a server-count with. + +.OUTPUTS + Returns the number of servers. Any non-zero percentage will return at least 1 server +#> + [CmdletBinding()] + [OutputType([System.Int32])] + param( + [Parameter(Mandatory = $true)] + [int]$ServerCount, + [Parameter(Mandatory = $true)] + [int]$Percentage + ) + + # Clamp the percentage between 0-100 + if($Percentage -gt 100) { + $Percentage = 100 + } + + # If the percentage is less than or equal to 0, just return 0. + # We can't assume that they want a min of 1 server here. + if($Percentage -le 0) { + return 0; + } + + # If the server count is zero, early out. + if($ServerCount -eq 0) { + return 0 + } + + # Figure out how many servers the percentage represents rounded down, and return. + $normalizedPercentage = [float]$Percentage / 100.0 + $numServers = $ServerCount * $normalizedPercentage # Get number of servers + $numServers = [math]::Floor($numServers) # Round down + $numServers = [math]::Max($numServers, 1) # Minimum of 1 server + return $numServers +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.tests.ps1 new file mode 100644 index 0000000..07159ba --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-PercentageServerCount.tests.ps1 @@ -0,0 +1,77 @@ +#. $PSScriptRoot\..\..\Load-PesterModules.ps1 +#$here = Split-Path -Parent $MyInvocation.MyCommand.Path +#$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +#$functionPath = Join-Path -Path $here -ChildPath $sut +#Write-Host "Overriding SUT: $functionPath" +#Import-Module $functionPath -Force +#$moduleForMock = "" + +Describe "Get-PercentageServerCount" { + + Context "ByServerCount" { + + It "0 Servers" { + + $serverCount = 0 + $percentage = 50 + $result = Get-PercentageServerCount -ServerCount $serverCount -Percentage $percentage + $result | Should -Be 0 + } + + It "0% of 4 is 0" { + # Minimum server count of 1 doesn't apply to 0% + $serverCount = 4 + $percentage = 0 + $result = Get-PercentageServerCount -ServerCount $serverCount -Percentage $percentage + $result | Should -Be 0 + } + + It "Any >0% returns 1" { + # Make sure min server count applies. + $serverCount = 4 + $percentage = 1 + $result = Get-PercentageServerCount -ServerCount $serverCount -Percentage $percentage + $result | Should -Be 1 + } + + # Thresholds test. + $cases = @( + # 0% Threshold Tests + @{ ServerCount = 4; Percentage = -1; Expect = 0 }, + @{ ServerCount = 4; Percentage = 0; Expect = 0 }, + @{ ServerCount = 4; Percentage = 1; Expect = 1 }, + + # 25% Threshold Tests + @{ ServerCount = 4; Percentage = 24; Expect = 1 }, + @{ ServerCount = 4; Percentage = 25; Expect = 1 }, + @{ ServerCount = 4; Percentage = 26; Expect = 1 }, + + # 50% Threshold Tests + @{ ServerCount = 4; Percentage = 49; Expect = 1 }, + @{ ServerCount = 4; Percentage = 50; Expect = 2 }, + @{ ServerCount = 4; Percentage = 51; Expect = 2 }, + + # 75% Threshold Tests + @{ ServerCount = 4; Percentage = 74; Expect = 2 }, + @{ ServerCount = 4; Percentage = 75; Expect = 3 }, + @{ ServerCount = 4; Percentage = 76; Expect = 3 }, + + # 100% Threshold Tests + @{ ServerCount = 4; Percentage = 99; Expect = 3 }, + @{ ServerCount = 4; Percentage = 100; Expect = 4 }, + @{ ServerCount = 4; Percentage = 101; Expect = 4 } + ) + It "Threshold -/+ Tests." -TestCases $cases { + param($ServerCount, $Percentage, $Expect) + $result = Get-PercentageServerCount -ServerCount $ServerCount -Percentage $Percentage + $result | Should -Be $Expect -Because "$($Percentage)% of $ServerCount servers should be $Expect servers"; + } + + It "Large Test: 25% of 400 is 100" { + $serverCount = 400 + $percentage = 25 + $result = Get-PercentageServerCount -ServerCount $serverCount -Percentage $percentage + $result | Should -Be 100 + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.ps1 new file mode 100644 index 0000000..93509bd --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.ps1 @@ -0,0 +1,29 @@ +function Get-ProviderMappingFilePath { + +<# +.SYNOPSIS + Returns the full path to the embedded PackageFiltering-min JSON file + +.DESCRIPTION + Returns the full path to the embedded PackageFiltering-min JSON file. Uses the FileList as defined in + the module PSM1 for location + +.OUTPUTS + Returns the full path to the bundled minified file. +#> + + [CmdletBinding()] + [OutputType([string])] + param() + + $logLead = Get-LogLeadName + $minifiedFile = $MyInvocation.MyCommand.Module.FileList | Where-Object {$_ -match "PackageFiltering-min.json"} + + if ($null -eq $minifiedFile -or (-NOT (Test-Path -Path $minifiedFile))) { + + Write-Warning "$logLead : Could not find bundled minified file. Check the module health or for build problems." + return $null + } + + return $minifiedFile +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.tests.ps1 new file mode 100644 index 0000000..3ed15c5 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFilePath.tests.ps1 @@ -0,0 +1,26 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ProviderMappingFilePath" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + + Context "Tests" { + + # Can't really test happy path, since we use a module-specifc construct to find the file + # + It "Writes a Warning and Returns Null if No File Found" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Get-ProviderMappingFilePath | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Could not find bundled minified file." } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.ps1 new file mode 100644 index 0000000..43dc174 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.ps1 @@ -0,0 +1,130 @@ +function Get-ProviderMappingFileUnion { + +<# +.SYNOPSIS + Compares configured providers to the Vanguard mapping file, and identifies chocolatey packages which are used/unused based on configuration and user input + +.DESCRIPTION + Compares configured providers to the Vanguard mapping file, and identifies chocolatey packages which are used/unused based on configuration. Queries + provider information from all tenants in the specified master database, compares to the Vanguard mapping file, and returns provider and package information + for those which can be safely disabled or enabled, based on user input + +.PARAMETER MasterConnectionString + The full Master databse connection string. Defaults to the local ConnectionString from the machine.config file + +.PARAMETER OverrideEnabledProviderPackageIds + Array of PackageIds to include in being enabled. This will be combined with return value from Get-AlwaysEnabledProviderPackageIds to force these to be returned in the "MatchedProviders" list + +.OUTPUTS + Unmatched provider and package data based on configured providers in all tenants + +.LINK + Get-AlwaysEnabledProviderPackageIds +#> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Parameter(Mandatory=$false)] + [string]$MasterConnectionString = (Get-ConnectionString "AlkamiMaster"), + + [Parameter(Mandatory = $false)] + [string[]]$OverrideEnabledProviderPackageIds = @(), + + [Parameter(Mandatory=$true, ParameterSetName = "MatchedProviders")] + [switch]$ReturnMatched, + + [Parameter(Mandatory=$true, ParameterSetName = "UnmatchedProviders")] + [switch]$ReturnUnmatched + ) + + $logLead = Get-LogLeadName + + if (Test-StringIsNullOrEmpty -Value $MasterConnectionString) { + + Write-Warning "$logLead : Supply a valid Master database connection string to this function and rerun" + return $null + } + + [PSObject[]]$providerMap = Read-ProviderMappingFile + + if (Test-IsCollectionNullOrEmpty -Collection $providerMap) { + + Write-Warning "$logLead : The provider mapping file returned no valid values. Exiting." + return + } else { + + Write-Host "$logLead : $($providerMap.Count) providers read from the provider mapping file" + } + + [PSObject[]]$providers = Read-ProviderConfigurationFromTenants -MasterConnectionString $MasterConnectionString + Write-Verbose "$logLead : $($providers.Count) unique providers read from the tenants" + + [array]$alwaysEnabledProviderPackageIds = Get-AlwaysEnabledProviderPackageIds + [array]$packageIdsToAlwaysReturnMatched = $OverrideEnabledProviderPackageIds + $alwaysEnabledProviderPackageIds + + foreach ($provider in $providers) { + + # Yes, more than one package might use the same provider record + [array]$matchingProviders = $providerMap | Where-Object { + ( + $_.ProviderName -eq $provider.ProviderName -and $_.ProviderType -eq $provider.ProviderType + ) -or + ( + $_.PackageId -in $packageIdsToAlwaysReturnMatched + ) + } + + if ($null -ne $matchingProviders) { + + # Log matched providers, but don't log if they're the always matched ones. That's just annoying + # Yes I know we loop over this again right after this, but I don't want any error in the functions there to screw up what should be a clean console log + $matchingProvidersWithoutAlwaysMatched = $matchingProviders | Where-Object {$_.PackageId -notin $packageIdsToAlwaysReturnMatched} + + if (-NOT (Test-IsCollectionNullOrEmpty $matchingProvidersWithoutAlwaysMatched)) { + if ($matchingProvidersWithoutAlwaysMatched.Count -gt 1) { + + Write-Host "$logLead : Matched provider [$($provider.ProviderName)] to multiple packages:" + $matchingProvidersWithoutAlwaysMatched | ForEach-Object { Write-Host "`t$($_.PackageId)"} + + } else { + + Write-Host "$logLead : Matched provider [$($provider.ProviderName)] to package [$($matchingProvidersWithoutAlwaysMatched | Select-Object -First 1 -ExpandProperty PackageId)]" + } + } + + foreach ($matchingProvider in $matchingProviders) { + + if ($null -eq $matchingProvider.PSObject.Properties["MatchCount"]) { + + Add-Member -InputObject $matchingProvider -TypeName Int32 -Name MatchCount -Value 1 -MemberType NoteProperty -Force + } else { + + $matchingProvider.MatchCount++ + } + } + } else { + + Write-Verbose "$logLead : The provider named [$($provider.ProviderName)] found in the databases was not matched to the mapping file by provider name and type, and will not be acted upon" + } + } + + [array]$unmatchedProviders = $providerMap | Where-Object {$null -eq $_.MatchCount} + [array]$matchedProviders = $providerMap | Where-Object {$_.MatchCount -gt 0} + + if ($ReturnMatched.IsPresent) { + + Write-Host "$logLead : Found $($matchedProviders.Count) provider(s) to enable based on mapping file." + return $matchedProviders + + } elseif ($ReturnUnmatched.IsPresent) { + + Write-Host "$logLead : Found $($unmatchedProviders.Count) provider(s) to disable based on mapping file. $($matchedProviders.Count) valid provider(s) found and will remain enabled." + return $unmatchedProviders + + } else { + + Write-Warning "$logLead : How did you even get here? This needs to be reported as a bug, it should not be possible" + throw [System.NotImplementedException]::New("Neither Matched nor Unmatched was Specified") + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.tests.ps1 new file mode 100644 index 0000000..4b7bcc5 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-ProviderMappingFileUnion.tests.ps1 @@ -0,0 +1,90 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ProviderMappingFileUnion" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + Mock -CommandName Read-ProviderMappingFile -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Read-ProviderConfigurationFromTenants -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ConnectionString -ModuleName $moduleForMock -MockWith { return "FakeConnectionString" } + + Context "General Tests" { + + It "Writes a Warning and Exits Early if an Empty Master Connection String is Provided" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Get-ProviderMappingFileUnion -MasterConnectionString $null -ReturnUnmatched | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Supply a valid Master database connection string" } + Assert-MockCalled -CommandName Read-ProviderMappingFile -Times 0 -Exactly -Scope It + } + + It "Uses the Parameter Value for the MasterConnectionString to Lookup Providers" { + + Mock -CommandName Read-ProviderMappingFile -ModuleName $moduleForMock -MockWith { return @("c", "is", "for", "cookie") } + Mock -CommandName Read-ProviderConfigurationFromTenants -ModuleName $moduleForMock -MockWith {} + + $fakeConnectionString = "Server=localhost;InitialCatalog=LittleBobbyTables;" + Get-ProviderMappingFileUnion -MasterConnectionString $fakeConnectionString -ReturnUnmatched + Assert-MockCalled -CommandName Read-ProviderConfigurationFromTenants -Times 1 -Exactly -Scope It ` + -ParameterFilter { $MasterConnectionString -eq $fakeConnectionString } + } + } + + Context "Matching Logic" { + + Mock -CommandName Read-ProviderMappingFile -ModuleName $moduleForMock -MockWith { + + $providerA = @{ + PackageId = "I.Cant.Believe.Its.Not.Butter"; + ProviderName = "Margarine"; + ProviderType = "SpreadableSSO"; + } + + $providerB = @{ + PackageId = "2.Percent.Milk"; + ProviderName = "Mooooooooooo"; + ProviderType = "DoesABodyGood"; + } + + return @($providerA, $providerB) + } + + Mock -CommandName Read-ProviderConfigurationFromTenants -ModuleName $moduleForMock -MockWith { + + $providerA = New-Object PSObject -Property @{ + ProviderType = "SpreadableSSO"; + ProviderName = "Margarine"; + } + + $providerB = New-Object PSObject -Property @{ + ProviderType = "Cheese"; + ProviderName = "PepperJack"; + } + + return @($providerA, $providerB, $providerC) + } + + It "Only Returns Providers that Match in File and the Database When Unmatched is selected" { + + $results = Get-ProviderMappingFileUnion -ReturnUnmatched + $results | Should -HaveCount 1 + $results.ProviderType | Should -BeExactly "DoesABodyGood" + $results.ProviderName | Should -BeExactly "Mooooooooooo" + } + + It "Only Returns Providers that Match in File and the Database When Matched is selected" { + + $results = Get-ProviderMappingFileUnion -ReturnMatched + $results | Should -HaveCount 1 + $results.ProviderType | Should -BeExactly "SpreadableSSO" + $results.ProviderName | Should -BeExactly "Margarine" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.ps1 new file mode 100644 index 0000000..7f49eb5 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.ps1 @@ -0,0 +1,149 @@ +function Get-Route53HostedZoneIdList { + <# +.SYNOPSIS + Retrieves a list of Route 53 Hosted Zone IDs for a given AWS profile. + +.DESCRIPTION + Retrieves a list of Route 53 Hosted Zone IDs for a given AWS profile. Route 53 Hosted Zones are global within the account, so no region is required. + +.PARAMETER ProfileName + The AWS profile to use when making the request. + +.PARAMETER PrivacyStatusFilter + If provided, filters the results to include only public or only private Route 53 Hosted Zones. + +.PARAMETER IncludePoeManaged + If provided, will include Poe managed Hosted Zone IDs in the results. By default, Poe managed Hosted Zones are filtered from the result. + +.OUTPUTS + Return an array of Hosted Zone IDs. + Returns null if the AWS call throws an exception. + +.EXAMPLE + Get-Route53HostedZoneIdList -ProfileName 'temp-prod' + +/hostedzone/Z28CPXDEPL7DZU +/hostedzone/ZW1IHU4IIBARF +/hostedzone/ZXHA4L9S5JVFB +/hostedzone/Z2DCTD3P7TUU22 +/hostedzone/Z2QAXNXCDIP0BG +/hostedzone/Z2WPZKRBGG8D98 +/hostedzone/Z3TUFNB8OX3MZG +/hostedzone/Z2YPL3QOGJ0UA +/hostedzone/Z3JRWVSQT7US74 +/hostedzone/Z46IBKYBT9G0N +/hostedzone/Z28YYZ58UUDGA +/hostedzone/Z2H6O2L3OZDQ3M +/hostedzone/Z3NHME8QKZEFFJ +/hostedzone/Z2VCXJ6FU82AXY +/hostedzone/ZFUYPMV7L13IF +/hostedzone/Z1D2COO77E2O97 +/hostedzone/ZDM17LUHLTRB3 +/hostedzone/Z3CWPFCDELIINV +/hostedzone/ZO8O2WS9VWYR2 +/hostedzone/Z043147535HEDWFO4YKLF +/hostedzone/Z04465442W0EYEVZO1LN2 +/hostedzone/Z047070527YNZSP8SDPWR +/hostedzone/Z0465462VSRRL839M32E +/hostedzone/Z08230832FYFH11IJ3LPR +/hostedzone/Z085622615BV9RWIT9NSK +/hostedzone/Z06877481CPDZ38P7C22D + +.EXAMPLE + Get-Route53HostedZoneIdList -ProfileName 'temp-prod' -PrivacyStatusFilter $true + +/hostedzone/Z28CPXDEPL7DZU +/hostedzone/ZW1IHU4IIBARF +/hostedzone/ZXHA4L9S5JVFB +/hostedzone/Z28YYZ58UUDGA +/hostedzone/Z2H6O2L3OZDQ3M +/hostedzone/Z3NHME8QKZEFFJ +/hostedzone/ZFUYPMV7L13IF +/hostedzone/ZDM17LUHLTRB3 +/hostedzone/Z3CWPFCDELIINV +/hostedzone/Z043147535HEDWFO4YKLF +/hostedzone/Z04465442W0EYEVZO1LN2 +/hostedzone/Z047070527YNZSP8SDPWR +/hostedzone/Z0465462VSRRL839M32E +/hostedzone/Z08230832FYFH11IJ3LPR +/hostedzone/Z085622615BV9RWIT9NSK +/hostedzone/Z06877481CPDZ38P7C22D + +.EXAMPLE + Get-Route53HostedZoneIdList -ProfileName 'temp-prod' -PrivacyStatusFilter $false + +/hostedzone/Z2DCTD3P7TUU22 +/hostedzone/Z2QAXNXCDIP0BG +/hostedzone/Z2WPZKRBGG8D98 +/hostedzone/Z3TUFNB8OX3MZG +/hostedzone/Z2YPL3QOGJ0UA +/hostedzone/Z3JRWVSQT7US74 +/hostedzone/Z46IBKYBT9G0N +/hostedzone/Z2VCXJ6FU82AXY +/hostedzone/Z1D2COO77E2O97 +/hostedzone/ZO8O2WS9VWYR2 +#> + + [OutputType([string[]])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Nullable[bool]] $PrivacyStatusFilter = $null, + + [Parameter(Mandatory = $false)] + [switch] $IncludePoeManaged + ) + + $result = $null + $logLead = Get-LogLeadName + + Import-AWSModule + + try { + + # Attempt to retrieve the hosted zone list for the specified profile. + $hostedZoneList = Get-R53HostedZoneList -ProfileName $ProfileName + + # If the user provided a privacy status filter, apply it to the list. + if ( $PSBoundParameters.ContainsKey( 'PrivacyStatusFilter' ) ) { + + Write-Verbose "$logLead : Applying privacy status filter of '$PrivacyStatusFilter' to the results." + $hostedZoneList = $hostedZoneList | Where-Object { $PrivacyStatusFilter -eq $_.Config.PrivateZone } + } + + if ( $IncludePoeManaged ) { + + # If the user wanted all the hosted zones, including Poe managed zones, + # our job here is done; just give them back all the IDs. + [string[]]$result = $hostedZoneList.Id + + } else { + + # Unfortunately, we're excluding Poe managed Route 53 hosted zones. + # Poe managed zones have a special tag on them; AWS doesn't give us + # the tags in the hosted zone list result, so we have to crawl the zones + # looking for the tag. + # THANKS AWS! /s + [string[]] $result = @() + foreach ( $zone in $hostedZoneList ) { + + $curTags = Get-Route53HostedZoneTagList -ProfileName $ProfileName -ZoneId $zone.Id + $filteredTags = $curTags | Where-Object { ($_.Key -eq 'alk:dnspropagation') -and ($_.Value -eq 'poemanaged') } + if (Test-IsCollectionNullOrEmpty $filteredTags) { + $result += $zone.Id + } + } + } + + } catch { + + Write-Warning ( "{0} : {1} : ProfileName = '{2}', PrivacyStatusFilter = '{3}', IncludePoeManaged = '{4}'" -f $logLead, $_, $ProfileName, $PrivacyStatusFilter, $IncludePoeManaged ) + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.tests.ps1 new file mode 100644 index 0000000..7514a52 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneIdList.tests.ps1 @@ -0,0 +1,157 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +Describe 'Get-Route53HostedZoneIdList' { + + $testPayload = @( + [PSCustomObject] @{ + 'Id' = 'TestPrivate' + 'Config' = [PSCustomObject]@{ + 'PrivateZone' = $true + } + }, + [PSCustomObject]@{ + 'Id' = 'TestPublic' + 'Config' = [PSCustomObject]@{ + 'PrivateZone' = $false + } + } + ) + + $testNonPoeTags = New-Object Collections.Generic.List[pscustomobject] + $testNonPoeTags.Add( + [pscustomobject]@{ + Key = 'alk:testkey1' + Value = 'alk:testvalue1' + } + ) + $testNonPoeTags.Add( + [pscustomobject]@{ + Key = 'alk:testkey2' + Value = 'alk:testvalue2' + } + ) + + $testPoeTags = New-Object Collections.Generic.List[pscustomobject] + $testPoeTags.Add( + [pscustomobject]@{ + Key = 'alk:testkey1' + Value = 'alk:testvalue1' + } + ) + $testPoeTags.Add( + [pscustomobject]@{ + Key = 'alk:dnspropagation' + Value = 'poemanaged' + } + ) + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-Route53HostedZoneIdList.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-R53HostedZoneList -ModuleName $moduleForMock -MockWith { return $testPayload } + + Mock -CommandName Get-Route53HostedZoneTagList -ModuleName $moduleForMock -MockWith { + if ( $ZoneId -eq 'TestPrivate' ) { + return $testPoeTags + } else { + return $testNonPoeTags + } + } + + Context 'Input Validation' { + + It 'Profile Name Should Not Be Null' { + + { Get-Route53HostedZoneIdList -ProfileName $null } | Should -Throw + } + + It 'Profile Name Should Not Be Empty' { + + { Get-Route53HostedZoneIdList -ProfileName '' } | Should -Throw + } + + It 'Privacy Status Filter Should Not Be Null' { + + { Get-Route53HostedZoneIdList -ProfileName 'Test' -PrivacyStatusFilter $null } | Should -Throw + } + + It 'Privacy Status Filter Should Not Be Empty' { + + { Get-Route53HostedZoneIdList -ProfileName 'Test' -PrivacyStatusFilter '' } | Should -Throw + } + + It 'Privacy Status Filter Should Be Boolean' { + + { Get-Route53HostedZoneIdList -ProfileName 'Test' -PrivacyStatusFilter 'Test' } | Should -Throw + } + } + + Context 'Result Validation' { + + It 'Should Write Warning If AWS Call Fails' { + + Mock -CommandName Get-R53HostedZoneList -ModuleName $moduleForMock -MockWith { throw 'This is a test.' } + + Get-Route53HostedZoneIdList -ProfileName 'Test' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-R53HostedZoneList -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + Mock -CommandName Get-R53HostedZoneList -ModuleName $moduleForMock -MockWith { return $testPayload } + } + + It 'Should Return Null If AWS Call Fails' { + + Mock -CommandName Get-R53HostedZoneList -ModuleName $moduleForMock -MockWith { throw 'This is a test.' } + + ( Get-Route53HostedZoneIdList -ProfileName 'Test' ) | Should -BeNull + + Mock -CommandName Get-R53HostedZoneList -ModuleName $moduleForMock -MockWith { return $testPayload } + } + + It 'Should Not Write Warning If AWS Call Succeeds' { + + Get-Route53HostedZoneIdList -ProfileName 'Test' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should Return All Results When Privacy Status Filter Is Not Provided' { + + $result = Get-Route53HostedZoneIdList -ProfileName 'Test' -IncludePoeManaged + + $result | Should -Contain 'TestPrivate' + $result | Should -Contain 'TestPublic' + } + + It 'Should Return Only Private Results When Privacy Status Filter Is True' { + + $result = Get-Route53HostedZoneIdList -ProfileName 'Test' -PrivacyStatusFilter $true -IncludePoeManaged + + $result | Should -Contain 'TestPrivate' + $result | Should -Not -Contain 'TestPublic' + } + + It 'Should Return Only Public Results When Privacy Status Filter Is False' { + + $result = Get-Route53HostedZoneIdList -ProfileName 'Test' -PrivacyStatusFilter $false -IncludePoeManaged + + $result | Should -Not -Contain 'TestPrivate' + $result | Should -Contain 'TestPublic' + } + + It 'Should Not Return Poe Managed Zones By Default' { + + $result = Get-Route53HostedZoneIdList -ProfileName 'Test' + + $result | Should -Not -Contain 'TestPrivate' + $result | Should -Contain 'TestPublic' + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.ps1 new file mode 100644 index 0000000..d3abf31 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.ps1 @@ -0,0 +1,62 @@ +function Get-Route53HostedZoneTagList { + <# +.SYNOPSIS + Retrieves a list of tags for a Route 53 Hosted Zone in a given AWS profile. + +.DESCRIPTION + Retrieves a list of tags for a Route 53 Hosted Zone in a given AWS profile. Route 53 Hosted Zones are global within the account, so no region is required. + +.PARAMETER ProfileName + The AWS profile to use when making the request. + +.PARAMETER ZoneId + The target Route53 Hosted Zone ID. + +.OUTPUTS + Return a list of tag objects (ref: https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/Route53/TTag.html) + Returns null if the AWS call throws an exception. + +.EXAMPLE + Get-Route53HostedZoneTagList -ProfileName 'temp-mgmt' -ZoneId '/hostedzone/Z0733762FO2Y6VVLR7A' + +Key Value +--- ----- +alk:role infratools +alk:env mgmt +alk:project infra +alk:service ec2 +#> + + [OutputType([System.Collections.Generic.List[PSObject]])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ZoneId + ) + + $result = $null + $logLead = Get-LogLeadName + + Import-AWSModule + + try { + + # In most of their API responses, the Hosted Zone ID returned by AWS is in the format + # /hostedzone/AAAAAAAAAAAAAAAAAAAAA + # For this endpoint, AWS doesn't want that prefix applied, so strip it down to match their expectation. + $resourceId = ($ZoneId -split '/')[-1] + + $result = (Get-R53TagsForResource -ResourceId $resourceId -ResourceType 'hostedzone' -ProfileName $ProfileName).Tags + + } catch { + + Write-Warning ( "{0} : {1} : ProfileName = '{2}', ZoneId = '{3}'" -f $logLead, $_, $ProfileName, $ZoneId ) + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.tests.ps1 new file mode 100644 index 0000000..7df3c20 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-Route53HostedZoneTagList.tests.ps1 @@ -0,0 +1,103 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +Describe 'Get-Route53HostedZoneTagList' { + + $testTags = New-Object Collections.Generic.List[pscustomobject] + $testTags.Add( + [pscustomobject]@{ + Key = 'alk:testkey1' + Value = 'alk:testvalue1' + } + ) + $testTags.Add( + [pscustomobject]@{ + Key = 'alk:testkey2' + Value = 'alk:testvalue2' + } + ) + + $testResult = [pscustomobject]@{ Tags = $testTags } + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-Route53HostedZoneTagList.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-R53TagsForResource -ModuleName $moduleForMock -MockWith { return $testResult } + + Context 'Input Validation' { + + It 'Profile Name Should Not Be Null' { + + { Get-Route53HostedZoneTagList -ProfileName $null } | Should -Throw + } + + It 'Profile Name Should Not Be Empty' { + + { Get-Route53HostedZoneTagList -ProfileName '' } | Should -Throw + } + + It 'Zone ID Should Not Be Null' { + + { Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId $null } | Should -Throw + } + + It 'Zone ID Should Not Be Empty' { + + { Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId '' } | Should -Throw + } + } + + Context 'Result Validation' { + + It 'Should Write Warning If AWS Call Fails' { + + Mock -CommandName Get-R53TagsForResource -ModuleName $moduleForMock -MockWith { throw 'This is a test.' } + + Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId 'TestZoneId' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-R53TagsForResource -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + Mock -CommandName Get-R53TagsForResource -ModuleName $moduleForMock -MockWith { return $testResult } + } + + It 'Should Return Null If AWS Call Fails' { + + Mock -CommandName Get-R53TagsForResource -ModuleName $moduleForMock -MockWith { throw 'This is a test.' } + + ( Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId 'TestZoneId' ) | Should -BeNull + + Assert-MockCalled -CommandName Get-R53TagsForResource -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + Mock -CommandName Get-R53TagsForResource -ModuleName $moduleForMock -MockWith { return $testResult } + } + + It 'Should Not Write Warning If AWS Call Succeeds' { + + Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId 'TestZoneId' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-R53TagsForResource -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It 'Should Return a List Containing All Tags' { + + $result = Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId 'TestZoneId' + + $result | Should -HaveCount $testTags.Count + } + + It 'Should Format the Input Zone ID to Appease AWS' { + + Get-Route53HostedZoneTagList -ProfileName 'Test' -ZoneId '/hostedzone/TestZoneId' | Out-Null + + Assert-MockCalled -CommandName Get-R53TagsForResource -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Resourceid -eq 'TestZoneId' } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-SupportedAwsRegions.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-SupportedAwsRegions.ps1 new file mode 100644 index 0000000..af8b7e0 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-SupportedAwsRegions.ps1 @@ -0,0 +1,11 @@ +function Get-SupportedAwsRegions { + <# + .SYNOPSIS + Returns a list of AWS regions currently considered as supported. This function does not return a complete list of every AWS region. + #> + [CmdletBinding()] + [OutputType([System.Object[]])] + param() + + return @("us-east-1", "us-west-2") +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-TenantTimeZone.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-TenantTimeZone.ps1 new file mode 100644 index 0000000..476ecbc --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-TenantTimeZone.ps1 @@ -0,0 +1,53 @@ +function Get-TenantTimeZone { + <# +.SYNOPSIS + This function queries the time zone name (TimeZoneInfo) for a FI given a tenant connection string. + +.PARAMETER TenantConnectionString + [string] The connection string for the tenant FI we want to query a timezone from. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$TenantConnectionString + ) + + try { + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $TenantConnectionString + $sqlConnection.Open() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + + # Define query to get the bank timezone name. + $command.CommandText = "select top 1 TimeZoneInfo from core.Bank" + + # Execute the command and parse the results out to $data + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + $data = @() + while ($reader.Read()) { + $data += @{ + TimeZoneInfo = $reader[0] + } + } + + # Each query only has one row result. + $record = $data[0] + if([string]::IsNullOrWhiteSpace($record.TimeZoneInfo)) { + Write-Error "$loglead : Could not find a configured timezone for FI $($tenant.Name)" + } else { + # Return the TimeZoneInfo name. + return $record.TimeZoneInfo + } + } catch { + Write-Error "$logLead : An error occurred querying for the timezone from tenant $($tenant.Name)" + Write-Host $_.Exception.Message + } finally { + # Clean up database connection. + if($null -ne $reader) { + $reader.Dispose() + } + if($null -ne $sqlConnection) { + $sqlConnection.Dispose(); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.ps1 new file mode 100644 index 0000000..12d2f71 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.ps1 @@ -0,0 +1,73 @@ +function Get-VpcIdList { + +<# +.SYNOPSIS + Retrieves a list VPC IDs for a given AWS region and profile. + +.PARAMETER ProfileName + [string] The AWS profile to use when making the request. + +.PARAMETER Region + [string] The AWS region to use when making the request. + +.PARAMETER IncludeDefault + [switch] Flag indicating to include the default VPC in the output. By default, the default VPC is omitted as it should be unused (if it exists). + +.EXAMPLE + Get-VpcIdList -ProfileName 'temp-dev' -Region 'us-east-1' + +vpc-f7cc3a8c +vpc-01d65e5a60e36e8b4 +#> + + [OutputType([string[]])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $false)] + [switch] $IncludeDefault + ) + + $result = $null + $logLead = (Get-LogLeadName) + + Import-AWSModule + + $filters = @() + + # Only include available VPCs. + $stateFilter = (New-Object Amazon.EC2.Model.Filter) + $stateFilter.Name = "state" + $stateFilter.Value = "available" + $filters += $stateFilter + + if ( $false -eq $IncludeDefault.IsPresent ) { + + # Omit the default VPC unless the user specifically asked for it. + # Alkami should not be using the default VPC; in fact, the default VPC shouldn't even exist. + Write-Verbose "$logLead : Applying isDefault filter to the results." + $defaultfilter = (New-Object Amazon.EC2.Model.Filter) + $defaultfilter.Name = "isDefault" + $defaultfilter.Value = "false" + $filters += $defaultfilter + } + + try { + + # Attempt to retrieve the VPC ID list for the specified profile and region. + $result = ( Get-EC2Vpc -ProfileName $ProfileName -Region $Region -Filter $filters ).VpcId + + } catch { + + Write-Warning ( "{0} : {1} : ProfileName = '{2}', Region = '{3}'" -f $logLead, $_, $ProfileName, $Region ) + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.tests.ps1 new file mode 100644 index 0000000..1dabf0b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Get-VpcIdList.tests.ps1 @@ -0,0 +1,80 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 + +Describe "Get-VpcIdList" { + + Mock -CommandName Get-AWSRegion -ModuleName Alkami.DevOps.Operations -MockWith { + return @( + @{ 'Region' = 'us-east-1' }, + @{ 'Region' = 'us-west-2' } + ) + } + + Mock -CommandName Import-AWSModule -ModuleName Alkami.DevOps.Operations -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName Alkami.DevOps.Operations -MockWith { return 'Get-VpcIdList.tests' } + + Context "Input Validation" { + + It "Profile Name Should Not Be Null" { + + { Get-VpcIdList -ProfileName $null } | Should -Throw + } + + It "Profile Name Should Not Be Empty" { + + { Get-VpcIdList -ProfileName '' } | Should -Throw + } + + It "Region Should Not Be Null" { + + { Get-VpcIdList -ProfileName 'Test' -Region $null } | Should -Throw + } + + It "Region Should Not Be Empty" { + + { Get-VpcIdList -ProfileName 'Test' -Region '' } | Should -Throw + } + + It "Region Should Be In Supported Regions List" { + + { Get-VpcIdList -ProfileName 'Test' -Region 'Test' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Write Warning If AWS Call Fails" { + + Mock -CommandName Get-EC2Vpc -ModuleName Alkami.DevOps.Operations -MockWith { throw "This is a test." } + Mock -CommandName Write-Warning -ModuleName Alkami.DevOps.Operations -MockWith {} + + Get-VpcIdList -ProfileName 'Test' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Return Null If AWS Call Fails" { + + Mock -CommandName Get-EC2Vpc -ModuleName Alkami.DevOps.Operations -MockWith { throw "This is a test." } + Mock -CommandName Write-Warning -ModuleName Alkami.DevOps.Operations -MockWith {} + + ( Get-VpcIdList -ProfileName 'Test' -Region 'us-east-1' ) | Should -BeNull + } + + It "Should Not Write Warning If AWS Call Succeeds" { + + Mock -CommandName Get-EC2Vpc -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'VPCId' = 'Test' } ) } + Mock -CommandName Write-Warning -ModuleName Alkami.DevOps.Operations -MockWith {} + + Get-VpcIdList -ProfileName 'Test' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Return Success When AWS Call Succeeds" { + + Mock -CommandName Get-EC2Vpc -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'VPCId' = 'Test' } ) } + + ( Get-VpcIdList -ProfileName 'Test' -Region 'us-east-1' ) | Should -Contain 'Test' + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Install-FailedChocoPackages.ps1 b/Modules/Alkami.DevOps.Operations/Public/Install-FailedChocoPackages.ps1 new file mode 100644 index 0000000..d25a82a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Install-FailedChocoPackages.ps1 @@ -0,0 +1,86 @@ +function Install-FailedChocoPackages { + <# +.SYNOPSIS + Install chocolatey packages that should be on a host, but are missing for whatever reason + +.PARAMETER ChocoPackagesThatShouldBeInstalled + Chocolatey packages that are expected to be installed + +.PARAMETER BorgUri + Uri for BoRG if you want to query BoRG for what packages should be installed + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string[]]$ChocoPackagesThatShouldBeInstalled = @(), + + [Parameter(Mandatory = $false)] + [string]$BorgUri + ) + + $failedToInstall = @() + $reallyFailedToInstall = @() + $logLead = Get-LogLeadName + + if (Test-IsCollectionNullOrEmpty -Collection $ChocoPackagesThatShouldBeInstalled) { + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Reading Platform Elements Details from BoRG" + $ChocoPackagesThatShouldBeInstalled = Get-PlatformElementInventory -BorgUri $BorgUri + if (!($ChocoPackagesThatShouldBeInstalled.count -gt 0)) { + Write-Warning "$logLead : BoRG contents is empty!" + return + } + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : Done Reading Platform Elements Details from BoRG" + } + + try { + $installedChocoPackages = Get-LocallyInstalledChocoPackages -LimitOutput + foreach ($package in $chocoPackagesThatShouldBeInstalled) { + # Ignore blank lines from the deployChocoPackages file and verify if package version matches + if (!([string]::IsNullOrWhiteSpace($package)) -and !($installedChocoPackages -contains $package)) { + $failedToInstall += $package + } + } + + #region InstallMissingPackages + if ($failedToInstall.count -gt 0) { + Write-Host "$logLead : The following package(s) failed to install and will now be remediated: " + foreach ($package in $failedToInstall) { + Write-Host "$logLead : $package" + } + + $packagesToInstall = $failedToInstall -replace '\|', ' ' | Out-String # Out-String as Invoke-ChocoInstallPackages takes a String Argument + $EnvironmentKey = Get-AppSetting -appSettingKey "Environment.Name" + If ($null -eq $EnvironmentKey) { + Write-Error "$logLead : Unable to get Environment" + } else { + Invoke-ChocoInstallPackages -packagesText $packagesToInstall -environment $EnvironmentKey + } + + # Get the updated installed package list + $installedChocoPackages = Get-LocallyInstalledChocoPackages -LimitOutput + + foreach ($package in $failedToInstall) { + # Ignore blank lines from the deployChocoPackages file and verify if package version matches + if (!([string]::IsNullOrWhiteSpace($package)) -and !($installedChocoPackages -contains $package)) { + $reallyFailedToInstall += $package + } + } + + if ($reallyFailedToInstall.Length -gt 0) { + Write-Warning "$logLead : You'll need to manually remediate the following packages:" + foreach ($package in $reallyFailedToInstall) { + Write-Host ("$logLead : $package") + } + } else { + Write-Host "$logLead : Remediation successful." + } + + } else { + Write-Host "$logLead : Hooray! There don't appear to be any missed package installs on this server." + } + #endregion InstallMissingPackages + } catch { + Write-Error "$logLead : Unexpected error - $($_.Exception.Message)" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Install-WinTest.ps1 b/Modules/Alkami.DevOps.Operations/Public/Install-WinTest.ps1 new file mode 100644 index 0000000..92a1ff7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Install-WinTest.ps1 @@ -0,0 +1,48 @@ +function Install-WinTest { +<# +.SYNOPSIS + Backup Current WinTest + Copy New WinTest + Clean out old WinTest +#> + + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$targetDirectory = "C:\Tools\WinTest", + + [Parameter(Mandatory=$false)] + [string]$sourceDirectory = "C:\temp\deploy\WinTest" + ) + + $logLead = (Get-LogLeadName) + $installDirectory = Join-Path -Path $targetDirectory -ChildPath "Current" + $cleanupAgeDays = 90 + $cleanupDate = (Get-Date).AddDays(-$cleanupAgeDays) + + # Exit early + if (!(Test-Path -Path $sourceDirectory)) { + Write-Warning "New WinTest deploy folder not found - no action taken." + return + } + + Backup-WinTest + + # Copy new folder + if (Test-Path $installDirectory) { + Write-Warning "$logLead : Current WinTest already exists, SKIPPING install" + } else { + Write-Verbose ("$logLead : Copy WinTest files from {0} to {1}" -f $sourceDirectory,$installDirectory) + Copy-Item -Path $sourceDirectory -Destination $installDirectory -Recurse + } + # Clean old folders (wrong order?) + Write-Verbose ("$logLead : Clean up old WinTest folders") + $folders = Get-ChildItem -Path $targetDirectory -Exclude "Current" + foreach ($folder in $folders) { + if ($folder.LastWriteTime -lt $cleanupDate) { + Write-Verbose ("$logLead : Retiring {0} due to age" -f $folder.FullName) + Remove-Item -Path $folder.FullName -Recurse -Force + } + } +} + diff --git a/Modules/Alkami.DevOps.Operations/Public/Invoke-RollingScriptBlock.ps1 b/Modules/Alkami.DevOps.Operations/Public/Invoke-RollingScriptBlock.ps1 new file mode 100644 index 0000000..7a524fe --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Invoke-RollingScriptBlock.ps1 @@ -0,0 +1,1102 @@ +function Invoke-RollingScriptBlock { + <# + .SYNOPSIS + Executes an arbitrary script block against target servers. + Correctly handles load balancing, and application restarts, depending on the type of server. + + .PARAMETER Servers + The list of servers to apply the rolling script block against. + + .PARAMETER RemoteScriptBlock + Script block to execute against each of the servers. + + .PARAMETER AgentScriptBlock + Script to run on the initiating machine. + + .PARAMETER ShouldRunScriptBlock + Script block to determine if the operation is required against a server. Executes on the remote machine. + If the script block returns true the Script will be executed against the server, or false to skip the server. + This is useful to speed up re-runs of a script. + This is required if the $ReRun parameter is set. + + .PARAMETER ShouldForceRunScriptBlockNumTimes + Number of times to Force the scripts to run. + Overrides return value from $ShouldRunScriptBlock up to $MaxReRuns times + + .PARAMETER Arguments + Arguments to pass to the -Script parameter script. + + .PARAMETER ReRun + If true, will re-evaluate the ShouldRunScriptBlock to determine if the agent/remote script blocks need to be run multiple times. + + .PARAMETER CycleLoadBalancer + Set to true to take the server out of the load balancer, run the script, and put it back in. + + .PARAMETER CycleOrb + Set to true to stop Alkami Services on the server, run the script, and start the services back up. + + .PARAMETER CycleOrbLeaveStopped + Overrides the "start the services" part of CycleOrb parameter. Alkami Services will still be stopped, but will be left stopped after the script is run. + + .PARAMETER IgnoreNag + Set to true to ignore nag considerations. Defaults to false. If Nag is running critical jobs at the time we execute the server, the rolling script will fail. + + .PARAMETER IgnoreServicesFailures + A list (array) of services to not throw/fail on in Ping-AlkamiServices. + + .PARAMETER IgnoreWebTests + Set to true to disable web tier testing and validation. + + .PARAMETER ServerParallelism + The number of servers to run the script against at a time. If the level of parallelism is higher than the number of servers, the script will fail unless -Force is used. + + .PARAMETER ServerParallelismIsPercentage + Re-interprets the ServerParallelism parameter to be a percentage instead of a flat server count. + + .PARAMETER FilterToRunningServers + Filters the provided server list down to the machines that are running in EC2. + + .PARAMETER RebootMachine + Set to true to reboot the machine prior to starting services and putting the server back into the load balancer. + + .PARAMETER RebootTimeoutMinutes + The timeout in minutes to wait for the machine to reboot. + + .PARAMETER MaxReRuns + Reruns will be evaluated this many times, before throwing an exception. ShouldRunScriptBlock must return false as an exit-condition. + + .PARAMETER AwsProfile + The Aws Profile that can manage the environment pointed at by $Servers. + This is a required field if web tier tests are running. + + .PARAMETER Force + Specify the force parameter to ignore the level of parallelism safety check. + + .PARAMETER SlackHookUrl + URL for Slack integration. + + .PARAMETER SlackChannels + CSV string of Slack channels to send messages to. + + .PARAMETER SitesToSkip + A list (array) of sites to skip web testing on. Any leading http/https values from the supplied sites will be trimmed where this is used. + + .PARAMETER SiteThreads + Number of parallel threads to run when testing sites on a server. + + .PARAMETER WidgetThreads + Number of parallel threads to run when testing widgets on a site. + + .PARAMETER MaximumSitesToTest + Number of sites to run web-tests on a web server. Will randomly pick sites if the number of configured sites on a server is + larger than this parameter. + + .PARAMETER SlackUsername + Specify Slack username to post as. + + .PARAMETER WebTestFailureMessageLink + String of text that links to the Confluence article regarding Web Test Failures. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Scope = 'Function', Justification = 'Per https://github.com/PowerShell/PSScriptAnalyzer/issues/1504 this is a known regression, skip this validation step')] + Param( + [Parameter(Mandatory = $true)] + [string[]]$Servers, + [Parameter(Mandatory = $false)] + [ScriptBlock]$RemoteScriptBlock = $null, + [Parameter(Mandatory = $false)] + [ScriptBlock]$AgentScriptBlock = $null, + [Parameter(Mandatory = $false)] + [ScriptBlock]$ShouldRunScriptBlock = $null, + [Parameter(Mandatory = $false)] + [int]$ShouldForceRunScriptBlockNumTimes = 0, + [Parameter(Mandatory = $false)] + [object[]]$Arguments = $null, + [Parameter(Mandatory = $false)] + [bool]$ReRun = $false, + [Parameter(Mandatory = $false)] + [bool]$CycleLoadBalancer = $true, + [Parameter(Mandatory = $false)] + [bool]$CycleOrb = $true, + [Parameter(Mandatory = $false)] + [bool]$CycleOrbLeaveStopped = $false, + [Parameter(Mandatory = $false)] + [bool]$IgnoreNag = $false, + [Parameter(Mandatory = $false)] + [bool]$IgnoreWebTests = $false, + [Parameter(Mandatory = $false)] + [bool]$IgnoreWebTestFailures = $false, + [Parameter(Mandatory = $false)] + [string[]]$IgnoreTestServers = $null, + [Parameter(Mandatory = $false)] + [int]$ServerParallelism = 1, + [Parameter(Mandatory = $false)] + [switch]$ServerParallelismIsPercentage, + [Parameter(Mandatory = $false)] + [switch]$FilterToRunningServers, + [Parameter(Mandatory = $false)] + [switch]$RebootMachine, + [Parameter(Mandatory = $false)] + [int]$RebootTimeoutMinutes = 60, + [Parameter(Mandatory = $false)] + [int]$MaxReRuns = 3, + [Parameter(Mandatory = $false)] + [string]$AwsProfile = $null, + [Parameter(Mandatory = $false)] + [switch]$Force, + [Parameter(Mandatory = $false)] + [string]$SlackHookUrl, + [Parameter(Mandatory = $false)] + [string]$SlackChannels, + [Parameter(Mandatory = $false)] + [string[]]$SitesToSkip, + [Parameter(Mandatory = $false)] + [int]$SiteThreads = 4, + [Parameter(Mandatory = $false)] + [int]$WidgetThreads = 8, + [Parameter(Mandatory = $false)] + [int]$MaximumSitesToTest = 20, + [Parameter(Mandatory = $false)] + [string[]]$IgnoreServicesFailures, + [Parameter(Mandatory = $false)] + [string]$WebTestFailureMessageLink = "https://confluence.alkami.com/x/vpPuBQ", + [Parameter(Mandatory = $false)] + [string]$SlackUsername = "Rolling Patching" + ) + + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + $loglead = (Get-LogLeadName) + + if ($IgnoreServicesFailures.Length -ne $IgnoreServicesFailures.Count) { + Write-Warning "IgnoreServicesFailures variable is a string, not an array" + } + + # Sanity checks. + # Make sure there is actually a server list. + if (Test-IsCollectionNullOrEmpty $Servers) { + Write-Warning "$loglead : Empty server list. Returning." + return + } + + # TC squashes arrays. Re-inflate it into an array if it exists. + Write-Host "$logLead : Sites to skip param: $SitesToSkip" + + # Make sure that if any of the Slack params are set, that the other Slack parameters are filled in. + $slackEnabled = $false + $slackHookUrlProvided = ![string]::IsNullOrWhitespace($SlackHookUrl) + $slackChannelsProvided = ![string]::IsNullOrWhitespace($SlackChannels) + if ($slackHookUrlProvided -or $slackChannelsProvided) { + if (!($slackHookUrlProvided -and $slackChannelsProvided)) { + throw "Slack Hook URL and Channels must both be provided to produce Slack output." + } else { + $slackEnabled = $true + } + } + + # Make sure that if we are running web tests, that an aws profile is specified. + # We need it to properly identify the FI's / URL endpoints to test. + if (($IgnoreWebTests -eq $false) -and ([string]::IsNullOrWhiteSpace($AwsProfile))) { + throw "$loglead : AwsProfile must be specified if web tests are being executed. If you don't care, use -IgnoreWebTests:`$true" + } + + # Filter servers to the machines that are running, or write an error. + [array]$runningServers = Select-RunningEC2InstancesByHostname -Servers $Servers -ProfileName $AwsProfile + if ($FilterToRunningServers) { + $Servers = $runningServers + } elseif ($runningServers.Count -ne $Servers.Count) { + Write-Error "$loglead : Not all servers provided are running in EC2. Did you mean to -FilterToRunningServers?" + } + + # Get lists of the types of servers for convenience. + [array]$webServers = Get-ServerByType -Server $Servers -Type Web + [array]$appServers = Get-ServerByType -Server $Servers -Type App + $hasWebServers = !(Test-IsCollectionNullOrEmpty $webServers) + $hasAppServers = !(Test-IsCollectionNullOrEmpty $appServers) + + # If ServerParallelismIsPercentage is specified, distill the $ServerParallelism 0-100 percentage into a server count. + if ($ServerParallelismIsPercentage) { + #TODO: Do not overwrite Input params with new values keyed off of Input params. Create and use new Variable + $ServerParallelism = Get-PercentageServerCount -ServerCount $Servers.Count -Percentage $ServerParallelism + Write-Host "$loglead : Parallelism set to $ServerParallelism servers." + } + + # Pick a server to query information from. + $firstServer = $Servers | Select-Object -First 1 + + # Determine the environment type, and figure out if it's a Development or QA environment. + $environmentType = Get-AppSetting -Key "Environment.Type" -ComputerName $firstServer + $isDevOrQaEnvironment = ($environmentType -eq "Development") -or ($environmentType -eq "Qa") + $isStagingEnvironment = ($environmentType -like "*Staging*") + $isProductionEnvironment = ($environmentType -like "*Production*") + + #region Get_InstanceData + # Figure out which server runs nag, which servers we have turned on, and which servers are overflow servers. + Write-Host "$loglead : Fetching instance/tag data from the servers." + $instanceData = Invoke-Command -ComputerName $Servers -ScriptBlock { + + # Get instance data. + $computername = (Get-FullyQualifiedServerName) + $instanceId = (Get-CurrentInstanceId) + $tags = (Get-CurrentInstanceTags) + $instanceRegion = (Get-CurrentInstanceRegion) + + # Figure out if this machine is the nag server. + $isNagServer = (Test-IsAppServer) -and (Test-IsNagServer) + + # Figure out if this machine is an overflow server. + $overflowTagKey = "alk:overflow" + $overflowTag = $tags | Where-Object { ($_.Key -eq $overflowTagKey) -and ($_.Value -eq "true") } + $isOverflowServer = $null -ne $overflowTag + + # Figure out if this machine was started up for patching. + $startedMachineTagKey = "alk:startedByPatchJob" + $startedMachineTag = $tags | Where-Object { ($_.Key -eq $startedMachineTagKey) -and ($_.Value -eq "true") } + $isStartedMachine = ($null -ne $startedMachineTag) + + <# + $isMicOrFabServer (A) $isStartedMachine (B) $willGoIntoLoadBalancer $A -and $B $A -or $B ~$A -and $B ~$A -and ~$B + $false $false $true $false $false $false $true + $false $true $false $false $true $true $false + $true $false $false $false $true $false $false + $true $true $false $true $true $false $false + #> + + # Determine if the machine will go back into the load balancer in the end. + $isMicOrFabServer = (Test-IsMicroServer -ComputerName $computername) -or (Test-IsServiceFabricServer -ComputerName $computername) + # If we didn't start it for this job, and it's not a fab or a mic, it can go into the load balancer (mic|fab don't _have_ a LB at all) + $willGoIntoLoadBalancer = !$isStartedMachine -and !$isMicOrFabServer + + # If this is an app server, determine if the host file has a loopback or redirect for SymconnectMultiplexer + $FailedSymLoopBackCheck = $null + if (Test-IsAppServer -ComputerName $computername) { + $loadbalancerState = Get-LoadBalancerState $env:COMPUTERNAME + + $TestSymConnectNonLoopbackExists = Test-SymConnectNonLoopbackExists + if ($TestSymConnectNonLoopbackExists -eq $true ) { + Write-Warning "[SymLoopbackCheck] : SymconnectMultiplexer does NOT loopback!!!!!!!!" + if ($loadbalancerState -eq "Active") { + Write-Warning "[SymLoopbackCheck] : Server is in the loadbalancer, but will be left out !!!!!!!!" + $FailedSymLoopBackCheck = $true + } else { + Write-Warning "[SymLoopbackCheck] : Server is in not the loadbalancer, and will be left out !!!!!!!!" + $FailedSymLoopBackCheck = $false + $willGoIntoLoadBalancer = $false + } + } elseif ($TestSymConnectNonLoopbackExists -eq $false) { + Write-Host "[SymLoopbackCheck] : SymconnectMultiplexer does loopback, This is happy path." + $FailedSymLoopBackCheck = $false + } else { + Write-Warning "[SymLoopbackCheck] : Was unable to determine if host line is loopback or not!!!!!!!! " + $FailedSymLoopBackCheck = $true + Write-Warning "[SymLoopbackCheck] : Set FailedSymLoopBackCheck flag!!!!!!!!" + } + } else { + Write-Host "[SymLoopbackCheck] : Server is not an app server, Loopback does not matter." + $FailedSymLoopBackCheck = $false + } + + if ($null -eq $FailedSymLoopBackCheck) { + Write-Warning "[SymLoopbackCheck] : Fell through SymConnect Loopback Checks without setting state, check logic!!!!!!!! " + $FailedSymLoopBackCheck = $true + Write-Warning "[SymLoopbackCheck] : Set FailedSymLoopBackCheck flag!!!!!!!!" + } + + # Write out the results in a PSObject + $results = @{ + ComputerName = $computername + InstanceId = $instanceId + Region = $instanceRegion + Tags = $tags + IsNagServer = $isNagServer + IsOverflowServer = $isOverflowServer + MachineWasOff = $isStartedMachine + BelongsInLoadBalancer = $willGoIntoLoadBalancer + FailedSymLoopBackCheck = $FailedSymLoopBackCheck + StartingLoadbalancerState = $loadbalancerState + } + $results = New-Object PSObject -Property $results + return $results + } + #endregion Get_InstanceData + + + # If this is an app server, determine if we are going to allow execution to contiune or throw a fit + if (($isDevOrQaEnvironment -eq $false) -and ($isStagingEnvironment -eq $false)) { + $ThrowForFailedLoopbackCheck = $false + $loopbackCheckData = $instanceData.FailedSymLoopBackCheck -contains $true + if ($loopbackCheckData) { + foreach ($instance in $instanceData) { + if ($instance.FailedSymLoopBackCheck -eq $true) { + Write-Warning "$loglead[SymLoopbackCheck] : $($instance.ComputerName) failed loopback check" + $ThrowForFailedLoopbackCheck = $true + } + } + if ($ThrowForFailedLoopbackCheck) { + throw "$loglead : An app tier instance has failed the SymconnectMultiplexer loopback check" + } + } + } else { + Write-Host "$loglead[SymLoopbackCheck] : Not checking sym loopback fail results for dev/qa/staging" + } + + # Get the region, number of overflow servers, and number of formerly offline servers. + $awsRegion = $instanceData[0].Region + $numOverflowServers = ([array]($instanceData | Where-Object { $_.IsOverflowServer })).Count + $numOfflineServers = ([array]($instanceData | Where-Object { $_.MachineWasOff })).Count + + # Figure out if there is a dedicated nag server. + $hasDedicatedNagServer = $false + if ($hasAppServers -and ($appServers.Count - $numOverflowServers) -gt 3) { + $hasDedicatedNagServer = $true + } + + # If there is a dedicated nag server, find the server in the instance data + # and make sure it doesn't go back into the load balancer. + if ($hasDedicatedNagServer) { + $nagInstanceData = $instanceData | Where-Object { $_.IsNagServer } + if ($nagInstanceData) { + $nagInstanceData.BelongsInLoadBalancer = $false + } + } + + #region Handle_FullParallelism_Or_FullOffline_DisableLoadBalancerHandling + # If the environment is a 1x1x1, or the entire environment was offline, disable load balancer handling. + # This includes environments that were scaled down to a 1x1x1 + $numActiveServers = $Servers.Count - $numOfflineServers + if ($numActiveServers -eq 1) { + Write-Host "$loglead : Operating on 1 live server. Disabling load balancer handling." + $CycleLoadBalancer = $false + Write-Host "$logLead : OVERRIDE - Leave Orb Stopped and Ignore Web Tests" + #HACK: We should NOT be overriding input params like this, but we don't know before this step + # if we're operating on a 1x1x1 or scaled-in designation. + # This will probably bite us later. + #INFO: Just because THIS tier only has 1 active server doesn't mean we're in a 1x1x1 + #INFO: If we do have only 1 active server, we'll still patch it with 100% parallelism + $CycleOrbLeaveStopped = $true + $IgnoreWebTests = $true + $ServerParallelism = $Servers.Count #sigh this is ungood; fix it in refactor story + #HACK: I do NOT like putting this here, but + # We don't know in TeamCity scriptblocks if we're on a 1x1x1 or if we were scaled to 1x1x1 + # This was supposed to be a flag from in TeamCity that we're explicitly doing 100% parallelism and + # to override "normal" behaviors. I'm not sure this will ONLY do what we want. + Write-Host "##teamcity[setParameter name='enableBounce' value='true']" + + } elseif ($numOfflineServers -eq $Servers.Count) { + Write-Host "$logLead : The entire environment was offline prior to Invoke-RollingScriptBlock. Disabling load balancer handling." + $CycleLoadBalancer = $false + } elseif ($ServerParallelism -ge $numActiveServers) { + # The environment has more than 1 active server, and so we care about the env going down. + # Make sure that parallelism is not set higher than the number of active servers. + if ($Force.IsPresent) { + Write-Host "$loglead : Parallelism is higher than the number of servers, and -Force is specified. Executing against all servers." + $CycleLoadBalancer = $false + } else { + throw "$loglead : Parallelism is set higher than the number of servers. Use -Force to force this to run on all servers without load balancing." + } + } + #endregion Handle_FullParallelism_Or_FullOffline_DisableLoadBalancerHandling + + Write-Host "$loglead : Starting rolling script block." + Write-Host "$loglead : Machines: $($Servers -join ", ")" + Write-Host "$loglead : Handle Load Balancers and Orb: $CycleLoadBalancer" + Write-Host "$loglead : Rebooting Machines: $RebootMachine" + Write-Host "$loglead : Rebooting Timeout: $($RebootTimeoutMinutes)m" + Write-Host "$loglead : ReRun: $ReRun" + Write-Host "$loglead : Max ReRuns: $MaxReRuns" + Write-Host "$loglead : Ignore Web Tests: $IgnoreWebTests" + Write-Host "$loglead : Ignore Services Failures: $IgnoreServicesFailures" + Write-Host "$loglead : AwsProfile: $AwsProfile" + Write-Host "$loglead : AWSRegion: $awsRegion " + Write-Host "$loglead : Querying Data From $firstServer" + Write-Host "$loglead : Environment Type: $environmentType" + Write-Host "$loglead : Num Overflow Servers: $numOverflowServers" + Write-Host "$loglead : Has Dedicated Nag: $hasDedicatedNagServer" + Write-Host "$loglead : Force shouldRunScriptBlock Times: $ShouldForceRunScriptBlockNumTimes" + + #region Check_ASGHasEnoughRoom + # If we are manipulating the load balancer, check if there is enough room in the ASG to support this rolling operation. + # Only check for room in the ASG for web servers if it is a dev or QA environment. + if ($CycleLoadBalancer -and ($hasAppServers -or ($hasWebServers -and $isDevOrQaEnvironment))) { + + Write-Host "$logLead : Determining if the AutoScaling Group has enough room to support the parallelism of the Invoke-RollingScriptBlock operation." + + # Get autoscaling group configuration. + $firstServerInstanceData = $instanceData | Where-Object { $_.ComputerName -eq $firstServer } + + # Get the autoscaling group for the server. + $autoscalingGroupTagKey = "aws:autoscaling:groupName" + + # Find the EC2 instances in the ASG + $autoScalingGroupName = $firstServerInstanceData.Tags | Where-Object { $_.Key -eq $autoscalingGroupTagKey } | Select-Object -First 1 -ExpandProperty Value + $autoScalingGroupMembers = (Get-InstancesByTag -tags @{ $autoscalingGroupTagKey = $autoScalingGroupName } -ProfileName $AwsProfile -Region $awsRegion).InstanceId + + Write-Verbose "Autoscaling Group Members:" + $autoScalingGroupMembers | ForEach-Object { Write-Verbose "$loglead : $_" } + + # Get the autoscaling group and its configuration. + $asg = (Get-ASAutoScalingGroup -AutoScalingGroupName $autoScalingGroupName -Region $awsRegion -ProfileName $AwsProfile) + + # Get the instances currently in the ASG + $instances = (Get-ASAutoScalingInstance -InstanceID $autoScalingGroupMembers -Region $awsRegion -ProfileName $AwsProfile) + + # Figure out which services are in service vs standby. + [array]$inServiceInstances = $instances | Where-Object { ($_.LifecycleState -eq "InService") } | Select-Object -ExpandProperty InstanceId + [array]$standbyInstances = $instances | Where-Object { $inServiceInstances -notcontains $_.InstanceId } | Select-Object -ExpandProperty InstanceId + + # Get final asg config. + $asgConfig = @{ + Min = $asg.MinSize + Max = $asg.MaxSize + InServiceInstances = $inServiceInstances + StandbyInstances = $standbyInstances + InServiceCount = $inServiceInstances.Count + InStandbyCount = $standbyInstances.Count + } + + # Assume the servers that will go back into the load balancer is the desired asg count. + # This count includes the overflow / nag servers being removed from the count. + # Note: This is not the desired count from the ASG console. + # The ASG console desired count allows machines to be in standby without affecting the desired count, and + # this represents the machines that will be put into the load balancer by the end of the Invoke-RollingScriptBlock. + $targetCount = ([array]($instanceData | Where-Object { $_.BelongsInLoadBalancer })).Count + + # Validate that there is enough room in the load balancer to handle the cycling in/out. + $netAsgInstances = ($targetCount - $ServerParallelism) - $asgConfig.Min + $hasEnoughRoom = $netAsgInstances -ge 0 + + if (!$hasEnoughRoom) { + throw "$logLead : The autoscaling group $($autoScalingGroupName) does not have enough room to support a server parallelism of $ServerParallelism.`n + Min: $($asgConfig.Min)`n + Max: $($asgConfig.Max)`n + Parallelism: $ServerParallelism`n + InService Target Count: $targetCount`n + Dedicated Nag: $hasDedicatedNagServer`n + Server Deficit: Need room for $(-$netAsgInstances) server(s)." + } + } else { + Write-Host "$logLead : Skipping AutoScaling Group parallelism min/max room check." + } + #endregion Check_ASGHasEnoughRoom + + # If rerun is true, validate that a $ShouldRunScriptBlock is set. + if ($ReRun -and ($null -eq $ShouldRunScriptBlock)) { + throw "$logLead : If ReRun is `$true, the ShouldRunScriptBlock argument must be specified to provide an exit condition." + } + + $isRunningInTeamCity = (Test-IsTeamCityProcess) + if ($isRunningInTeamCity) { + Write-Host "$logLead : Determined that this is executing inside a TeamCity process." + } + + #region Set_InstanceHealth + # Set the health states of web/app servers to healthy. + if ($CycleLoadBalancer) { + $instancesToSetHealthy = @() + if ($hasWebServers -and (!$hasAppServers)) { + $instancesToSetHealthy = $webServers + } elseif ($hasAppServers -and (!$hasWebServers)) { + $instancesToSetHealthy = $appServers + } else { + $instancesToSetHealthy = [array]$webServers + [array]$appServers + } + + # Disable cycling the load balancer for webs if it's a DR environment, because they don't have a nginx server set up. + $serverToQuery = $instancesToSetHealthy | Select-Object -First 1 + $isDR = $false + if ($null -ne $serverToQuery) { + $environmentName = Get-AppSetting -Key "Environment.Name" -ComputerName $serverToQuery + $isDR = $environmentName -match "\bDR\b" + } + if ($isDR -and $hasWebServers) { + Write-Warning "$loglead : Disabling load balancer handling in DR because Set-NginxHostState does not know how to find DR nginx servers." + $CycleLoadBalancer = $false + } elseif (!(Test-IsCollectionNullOrEmpty $instancesToSetHealthy)) { + Write-Host "$loglead : Setting ASG instance states to Healthy for hosts: $($instancesToSetHealthy -join ", ")" + Reset-ASInstanceHealth -Servers $instancesToSetHealthy -ProfileName $AwsProfile + } + } + #endregion Set_InstanceHealth + + # Execute script against servers. + $serverScript = { + param($Server, $inputArguments) + + # Manually named because Get-LogLeadName won't work inside this script block. + $loglead = "[Invoke-RollingScriptBlock]" + + #region Argument_Handling + # Pull the user script & their arguments out of the arguments above. + $sbRemoteScript = $inputArguments.RemoteScriptBlock + $sbAgentScript = $inputArguments.AgentScriptBlock + $sbShouldRunScript = $inputArguments.ShouldRunScriptBlock + $sbShouldForceRunScriptNumTimes = $inputArguments.ShouldForceRunScriptBlockNumTimes + [array]$sbArguments = $inputArguments.Arguments + $sbCycleLoadBalancer = $inputArguments.CycleLoadBalancer + $sbCycleOrb = $inputArguments.CycleOrb + $sbCycleOrbLeaveStopped = $inputArguments.CycleOrbLeaveStopped + $sbIgnoreNag = $inputArguments.IgnoreNag + $sbIgnoreWebTests = $inputArguments.IgnoreWebTests + $sbIgnoreTestServers = $inputArguments.IgnoreTestServers + $sbRebootMachine = $inputArguments.RebootMachine + $sbRebootTimeoutMinutes = $inputArguments.RebootTimeoutMinutes + $sbReRun = $inputArguments.ReRun + $sbMaxReRuns = $inputArguments.MaxReRuns + $sbAwsProfile = $inputArguments.AwsProfile + $sbAwsRegion = $inputArguments.AwsRegion + $sbIsRunningInTeamCity = $inputArguments.IsRunningInTeamCity + $sbInstanceData = $inputArguments.InstanceData + $sbIgnoreWebTestFailures = $inputArguments.IgnoreWebTestFailures + $sbIsProductionEnvironment = $inputArguments.IsProductionEnvironment + $sbSlackHookUrl = $inputArguments.SlackHookUrl + $sbSlackChannels = $inputArguments.SlackChannels + $sbIsSlackEnabled = $inputArguments.IsSlackEnabled + $sbWebTestFailureMessageLink = $inputArguments.WebTestFailureMessageLink + $sbSlackUsername = $inputArguments.SlackUsername + $sbSitesToSkip = $inputArguments.SitesToSkip + $sbSiteThreads = $inputArguments.SiteThreads + $sbWidgetThreads = $inputArguments.WidgetThreads + $sbMaximumSitesToTest = $inputArguments.MaximumSitesToTest + [array]$sbIgnoreServicesFailures = $inputArguments.IgnoreServicesFailures + + # Deserialize the scripts into runnable script-blocks. + if ($null -ne $sbAgentScript) { + $sbAgentScript = [scriptblock]::Create($sbAgentScript) + } + if ($null -ne $sbRemoteScript) { + $sbRemoteScript = [scriptblock]::Create($sbRemoteScript) + } + if ($null -ne $sbShouldRunScript) { + $sbShouldRunScript = [scriptblock]::Create($sbShouldRunScript) + } + + # Modify the user arguments to include the server being operated on as the first argument. + $sbArguments = @($Server) + $sbArguments + #endregion Argument_Handling + + try { + # Write TeamCity open-block to give a dropdown breakdown by server. + if ($sbIsRunningInTeamCity) { + Write-Host "##teamcity[blockOpened name='$Server']" + } + + # Get the EnvironmentName from $server + $environmentName = Get-AppSetting -Key "Environment.Name" -ComputerName $Server + + # Figure out if the server ultimately belongs in the load balancer or not. + $serverInstanceData = $sbInstanceData | Where-Object { $Server -eq $_.ComputerName } | Select-Object -First 1 + if ($null -eq $serverInstanceData) { + throw "$loglead : Could not find instance data for server $Server. Is it missing a domain name?" + } + + $rebootedMachine = $false + $numReruns = 0 + $shouldExecuteScripts = $true + do { + #region ShouldExecuteScripts + # Determine if we should run the script against the server. + $shouldExecuteScripts = $true + if ($null -ne $sbShouldRunScript) { + $shouldExecuteScripts = Invoke-Command -ComputerName $Server -ScriptBlock $sbShouldRunScript -ArgumentList ($sbArguments) + if ($shouldExecuteScripts -eq $true) { + Write-Host "$loglead : Determined that we are executing the agent/remote scripts against $Server" + } else { + Write-Host "$loglead : Determined that the agent/remote scripts do not need to be executed against $Server" + } + } + + # Execute the scripts if we need to. + # Regardless of if we want to execute the script, the server will be started and put back into the load balancer. + # This is to handle cases where if a script fails while the server is out of the load balancer, that re-running the job will put that server back into the pool. + if (!$shouldExecuteScripts) { + if ($sbShouldForceRunScriptNumTimes -gt $numReruns) { + Write-Warning "OVERRIDE - Return value from ShouldRunScriptBlock being overridden" + Write-Warning "Forcing scripts to run this many times : $sbShouldForceRunScriptNumTimes" + $shouldExecuteScripts = $true + } else { + Write-Host "$logLead : Not executing script against $server." + break + } + } + #endregion ShouldExecuteScripts + + # Write rerun text. + if ($sbReRun) { + Write-Host "$logLead : Starting Run ($($numReruns+1)/$sbMaxReRuns)" + } + + #region Set_LoadBalancerState_Down + + $lbStateResult = $null + # Take the server out of the load balancer. Don't bother taking the server out of the LB after the first script execution. + if ($sbCycleLoadBalancer -and ($numReruns -eq 0) -and $serverInstanceData.BelongsInLoadBalancer) { + # If the LB change fails, this should *always* throw. Kill the whole process with fire. + try { + Write-Host "$logLead : Removing $server from the load balancer." + $lbStateResult = Set-LoadBalancerState -server $server -desiredState "down" -AwsProfileName $sbAwsProfile -AwsRegion $sbAwsRegion -Force + } catch { + throw "Setting Load Balancer State to Down failed. Throwing, explictly. `n$_" + } + + # The underlying signature of Set-LoadBalancerState changed, so it's not throwing. Handle the failure state here. + if ($lbStateResult -eq "Fail") { + throw "Setting Load Balancer State to Down failed. Throwing, explictly." + } + # Slack notification out of load balancer. + if ($sbIsSlackEnabled) { + # If slack fails, we should *not* fail. Messages are nice, but not a hill to die on. + try { + $messageText = "$environmentName [$Server] - Removed from load balancer" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName $sbSlackUsername + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + } catch { + Write-Host "Error caught attempting to send Out Load Balancer message to Slack! `n$_" + } + } + } + + #endregion Set_LoadBalancerState_Down + + #region Stop_AlkamiPlatform + # Stop services on the server. + # Continue to stop orb after multiple re-runs, incase the machine is rebooted and we need to stop automatic start services. + if ($sbCycleOrb) { + + #region Nag_Safety + $serverIsAppServer = ($null -ne (Select-AlkamiAppServers $Server)) + if ($serverIsAppServer -and (!$sbIgnoreNag)) { + + Write-Host "$logLead : Checking if Nag is running prior to stopping server $Server." + $nagJobsRunning = $false + + # Only do Nag checks in Production environments. + if (!$sbIsProductionEnvironment) { + Write-Host "$logLead : We do not care if Nag is running in non-production environments. Continuing.." + } elseif (Test-IsNagServer -Server $Server) { + Write-Host "$logLead : Determined that server $Server is the Nag server." + + # Grab the master connection string for nag checks. + $masterConnectionString = (Get-ConnectionString -name "AlkamiMaster" -ComputerName $Server) + + # Determine if Nag is within the time window where we do not care about force-restarting it for all FI's. + Write-Host "$logLead : Checking if all tenants are inside the 12:00am-6:00am time window where we do not care about restarting nag." + $canRestartNag = Test-IsCurrentTimeInsideTenantTimeRange -MasterConnectionString $masterConnectionString -StartTimeHour 0 -EndTimeHour 6 + + if ($canRestartNag) { + Write-Host "$logLead : All FI's are within the 12:00-6:00am time window. Restarting Nag." + } else { + # If we can't summarily restart nag, figure out if critical nag jobs are running. + + # Figure out if nag jobs are running on the remote machine. + # Note: Test-AreCriticalNagJobsRunning is poorly named. No jobs are running if it returns $true. + $serverIsNotRunningNagJobs = (Test-AreCriticalNagJobsRunning -masterConnectionString $masterConnectionString) + + # Test-AreCriticalNagJobsRunning returns $true or $null. Turn this into a proper bool. + if ($null -eq $serverIsNotRunningNagJobs) { + $serverIsNotRunningNagJobs = $false + } + $nagJobsRunning = !$serverIsNotRunningNagJobs + + if ($nagJobsRunning) { + throw "Cannot deploy to Nag server: $Server; there are critical jobs scheduled or running." + } else { + Write-Host "$logLead : There are no Nag jobs running. The server can be safely stopped" + } + } + } + } + #endregion Nag_Safety + + Write-Host "$logLead : Stopping services and clearing logs on $Server" + # Slack notification Stopping server. + if ($sbIsSlackEnabled) { + try { + $messageText = "$environmentName [$Server] - Stopping services and clearing logs" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName $sbSlackUsername + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + } catch { + Write-Host "Error caught attempting to send Stopping Services message to Slack!" + } + } + + + #region Stop_AlkamiPlatform_NestedSb_SuppressErrors + $nestedSbArguments = @{ + RebootMachine = $sbRebootMachine + IsRunningInTeamCity = $sbIsRunningInTeamCity + } + Invoke-Command -ComputerName $Server -ArgumentList $nestedSbArguments -ScriptBlock { + param($nestedSbInputArguments) + $nestedSbRebootMachine = $nestedSbInputArguments.RebootMachine + $nestedSbIsRunningInTeamCity = $nestedSbInputArguments.IsRunningInTeamCity + if ($nestedSbRebootMachine) { + #TODO: scope problem? + #region Stop_AlkamiPlatform_NestedSb_SuppressErrors + Write-Warning "SUPPRESSING ERRORS - reboot will follow 'Stop-Platform' process" + #do things + $params = @{} + $params.ErrorAction = "SilentlyContinue" + $params.ErrorVariable = "SuppressedErrors" + + # Stop services. + try { + Stop-IISAndServices @params + } catch { + $exceptionInfo = $_ + Write-Warning "Errors have been CAUGHT from something called by Stop-IISAndServices" + Write-Warning $exceptionInfo + } + if ($SuppressedErrors) { + Write-Warning "Errors in Stop-IISAndServices have been suppressed:" + $SuppressedErrors.ForEach( { + Write-Warning $_ + }) + # Reset ErrorVariable + $SuppressedErrors = $null + } + + + # Log everyone out of the host + try { + Revoke-LogonUsers @params + } catch { + $exceptionInfo = $_ + Write-Warning "Errors have been CAUGHT from something called by Revoke-LogonUsers" + Write-Warning $exceptionInfo + } + if ($SuppressedErrors) { + Write-Warning "Errors in Revoke-LogonUsers have been suppressed:" + $SuppressedErrors.ForEach( { + Write-Warning $_ + }) + # Reset ErrorVariable + $SuppressedErrors = $null + } + + + # Remove any locks to the filesystem + try { + Close-SMBApplicationLocks @params + } catch { + $exceptionInfo = $_ + Write-Warning "Errors have been CAUGHT from something called by Close-SMBApplicationLocks" + Write-Warning $exceptionInfo + } + if ($SuppressedErrors) { + Write-Warning "Errors in Close-SMBApplicationLocks have been suppressed:" + $SuppressedErrors.ForEach( { + Write-Warning $_ + }) + # Reset ErrorVariable + $SuppressedErrors = $null + } + + + # Clean up .NET temps + try { + Remove-DotNetTemporaryFiles @params + } catch { + $exceptionInfo = $_ + Write-Warning "Errors have been CAUGHT from something called by Remove-DotNetTemporaryFiles" + Write-Warning $exceptionInfo + } + if ($SuppressedErrors) { + Write-Warning "Errors in Remove-DotNetTemporaryFiles have been suppressed:" + $SuppressedErrors.ForEach( { + Write-Warning $_ + }) + # Reset ErrorVariable + $SuppressedErrors = $null + } + + + # Clean up our logs dir + try { + Backup-ORBLogFiles -SkipActive @params + } catch { + $exceptionInfo = $_ + Write-Warning "Errors have been CAUGHT from something called by Backup-ORBLogFiles" + Write-Warning $exceptionInfo + } + if ($SuppressedErrors) { + Write-Warning "Errors in Backup-ORBLogFiles have been suppressed:" + $SuppressedErrors.ForEach( { + Write-Warning $_ + }) + # Reset ErrorVariable + $SuppressedErrors = $null + } + #endregion Stop_AlkamiPlatform_NestedSb_SuppressErrors + } else { + Write-Warning "Not suppressing errors - reboot not planned" + Stop-IISAndServices + Revoke-LogonUsers + Close-SMBApplicationLocks + Remove-DotNetTemporaryFiles + Backup-ORBLogFiles -SkipActive -ErrorAction "Continue" + } + + if ($nestedSbIsRunningInTeamCity) { + Write-Host "##teamcity[setParameter name='didStopAlkamiPlatform' value='YES']" + } + } + #endregion Stop_AlkamiPlatform_NestedSb_SuppressErrors + } + #endregion Stop_AlkamiPlatform + + # Execute the agent script. + if ($null -ne $sbAgentScript) { + Write-Host "$logLead : Executing agent script block for $server" + Invoke-Command -ScriptBlock $sbAgentScript -NoNewScope -ArgumentList ($sbArguments) + } + + # Execute the remote script. + if ($null -ne $sbRemoteScript) { + Write-Host "$logLead : Executing remote script block on $server" + Invoke-Command -ScriptBlock $sbRemoteScript -ComputerName $Server -ArgumentList ($sbArguments) + } + + #region RebootMachine + # Reboot the machine. + if ($sbRebootMachine) { + Write-Host "$logLead : Rebooting $server" + $rebootedMachine = $true + # Slack notification Restart server. + if ($sbIsSlackEnabled) { + try { + $messageText = "$environmentName [$Server] - Rebooting server" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName $sbSlackUsername + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + } catch { + Write-Host "Error caught attempting to send Restart Server message to Slack!" + } + } + + Restart-Computer -ComputerName $Server -Wait -Timeout ($sbRebootTimeoutMinutes * 60) -Delay 10 -Protocol WSMan -Force + } + #endregion RebootMachine + + # If we are doing reruns, keep looping. + } while ($sbReRun -and ((++$numReruns) -lt $sbMaxReRuns)) + + #region TooManyRetries + # If we reached the max rerun attempts, and $shouldExecuteScripts is true, figure out if the job thinks it has finished. + if ($sbReRun -and ($shouldExecuteScripts -eq $true)) { + $shouldExecuteScripts = Invoke-Command -ComputerName $Server -ScriptBlock $sbShouldRunScript -ArgumentList ($sbArguments) + if ($shouldExecuteScripts) { + throw "$logLead : Maximum $sbMaxReRuns retry attempts reached. Please re-run the job, or investigate the ShouldRunScriptBlock exit-condition. The server has not been started back up." + } + } + #endregion TooManyRetries + + #region Start_AlkamiPlatform + # Start services on the server. + # Skip if CycleOrbLeaveStopped is TRUE (I dislike this logic inversion. Naming is hard and this should be the - hopefully rare - exception) + # Changing to "-not" notation to make them stick out more. The number of flags and beacons we have going on + # here makes reasoning about it hard enough without needing to spot "!" hiding next to "(" + # using ALLCAPS for the same visibility reasoning + # Don't bother starting the server if it was off prior to Invoke-RollingScriptBlock. + if ($sbCycleOrb -and -NOT $sbCycleOrbLeaveStopped -and -NOT $serverInstanceData.MachineWasOff) { + Write-Host "$logLead : Starting services on $server" + + # Slack notification Starting server. + if ($sbIsSlackEnabled) { + try { + $messageText = "$environmentName [$Server] - Starting services" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName $sbSlackUsername + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + } catch { + Write-Host "Error caught attempting to send Starting Server message to Slack!" + } + } + + # Define a script to start up the server. + # In the case of a user-script failure, the server will be started and put back into the load balancer on re-runs. + $startupScript = { + param ( + $nestedSb_logLead, + $nestedSbIgnoreServicesFailures + ) + + Write-Host "$nestedSb_logLead : StartupScript : Prepare to run" + + Clear-GMSAPasswords + Start-IISAndServices + if (Test-IsWebServer) { + Ping-AlkamiWebSites + Start-Sleep -Seconds 60 + } elseif (Test-IsAppServer) { + $pingAlkServices = Ping-AlkamiServices + foreach ($serviceResult in $pingAlkServices) { + if ($serviceResult.WebAppName -in $nestedSbIgnoreServicesFailures) { + Write-Host "$nestedSb_logLead : Skipping evaluation of ping results for $($serviceResult.WebAppName) because it was given as an IgnoreServicesFailures parameter value" + continue + } + + if ($serviceResult.Success -eq $false) { + $description = ConvertTo-SafeTeamCityMessage -InputText "$($serviceResult.Url) failed the ping check, was not set for ignoring" + $identity = ConvertTo-SafeTeamCityMessage -InputText "$($serviceResult.WebAppName)_pingFailed" + + Write-Host "##teamcity[buildProblem description='$description' identity='$identity']" + } + } + } + } + + # If the machine was rebooted we want a full restart-tier to make sure the environment is in a good state. + if ($rebootedMachine) { + $startupScript = { + param ( + $nestedSb_logLead, + $nestedSbIgnoreServicesFailures + ) + Write-Host "$nestedSb_logLead : StartupScript-RebootedMachine : Prepare to run" + Clear-GMSAPasswords + $pingServicesServiceResults = Restart-Tier + if (Test-IsAppServer) { + foreach ($serviceResult in $pingServicesServiceResults) { + if ($serviceResult.WebAppName -in $nestedSbIgnoreServicesFailures) { + Write-Host "$nestedSb_logLead : Skipping evaluation of ping results for $($serviceResult.WebAppName) because it was given as an IgnoreServicesFailures parameter value" + continue + } + + if ($serviceResult.Success -eq $false) { + $description = ConvertTo-SafeTeamCityMessage -InputText "$($serviceResult.Url) failed the ping check, was not set for ignoring" + $identity = ConvertTo-SafeTeamCityMessage -InputText "$($serviceResult.WebAppName)_pingFailed" + + Write-Host "##teamcity[buildProblem description='$description' identity='$identity']" + } + } + } else { + # Just dump the results as it would have done instead, so we can see them + $pingServicesServiceResults + } + if (Test-IsWebServer) { + Start-Sleep -Seconds 60 + } + } + } + + Invoke-Command -ComputerName $Server -ScriptBlock $startupScript -ArgumentList @($loglead, $sbIgnoreServicesFailures) + } elseif ($sbCycleOrb -and $sbCycleOrbLeaveStopped -and -NOT $serverInstanceData.MachineWasOff) { + Write-Warning -Message "$logLead : OVERRIDE - NOT starting Alkami Services due to passed param -CycleOrbLeaveStopped" + if ($sbIsRunningInTeamCity) { + Write-Host "##teamcity[setParameter name='didLeaveAlkamiPlatformStopped' value='YES']" + } + } + #endregion Start_AlkamiPlatform + + #region Invoke_WebTests + # If it's a web server, we are running the web tests, the machine wasn't off prior, and the server is not in the ignore-test list. + $serverIsWebServer = ($null -ne (Select-AlkamiWebServers $Server)) + if ($serverIsWebServer -and (!$sbIgnoreWebTests) -and (!$serverInstanceData.MachineWasOff) -and (!($sbIgnoreTestServers -contains $Server))) { + $testsPassed = (Invoke-WebTests -Servers $Server ` + -AwsProfile $sbAwsProfile ` + -SitesToSkip $sbSitesToSkip ` + -MaximumSitesToTest $sbMaximumSitesToTest ` + -SiteThreads $sbSiteThreads ` + -WidgetThreads $sbWidgetThreads) + # Only throw if we want to ignore test failures and the tests failed. + if (!$sbIgnoreWebTestFailures -and !$testsPassed) { + # Slack output Web Tests Failed. + if ($sbIsSlackEnabled) { + try { + $WebTestFailureUsername = $sbSlackUsername + " Failure" + $messageText = "$environmentName [$Server] - FAILED web tests! For guidance and Q&A, see <$sbWebTestFailureMessageLink|$sbSlackUsername Job Documentation>" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":alert:" -UserName $WebTestFailureUsername + $webFailSlackChannels = $sbSlackChannels + + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $webFailSlackChannels + } catch { + Write-Host "Error caught attempting to send Web Tests Failed message to Slack!" + } + } + + throw "$logLead : Web tier testing of server $Server failed. Investigate! For guidance and Q&A, see $sbWebTestFailureMessageLink" + } + + # Take a break and give the server time to settle after the tests, before putting back into the LB. + Start-Sleep -Seconds 60 + } + #endregion Invoke_WebTests + + #region Set_LoadBalancerState_Up + # Put the server back into the load balancer. + $lbStateResult = $null + if ($sbCycleLoadBalancer -and $serverInstanceData.BelongsInLoadBalancer) { + # If the LB change fails, this should *always* throw. Kill the whole process with fire. + try { + Write-Host "$logLead : Adding $server back into the load balancer" + $lbStateResult = Set-LoadBalancerState -server $server -desiredState "up" -AwsProfileName $sbAwsProfile -AwsRegion $sbAwsRegion -Force + } catch { + throw "Setting Load Balancer State to Up failed. Throwing, explictly. `n$_" + } + + # The underlying signature of Set-LoadBalancerState changed, so it's not throwing. Handle the failure state here. + if ($lbStateResult -eq "Fail") { + throw "Setting Load Balancer State to Down failed. Throwing, explictly." + } + # Slack output added back into Load Balancer. + if ($sbIsSlackEnabled) { + # If slack fails, we should *not* fail. Messages are nice, but not a hill to die on. + try { + $messageText = "$environmentName [$Server] - Added to load balancer" + $slackMessage = Format-SlackMessage -MessageText $messageText -IconEmoji ":teamcity:" -UserName $sbSlackUsername + Publish-MessageToSlack -MessageBody $slackMessage -slackHookUrl $sbSlackHookUrl -channels $sbSlackChannels + } catch { + Write-Host "Error caught attempting to send In Load Balancer message to Slack! `n$_" + } + } + } + #endregion Set_LoadBalancerState_Up + } finally { + if ($sbIsRunningInTeamCity) { + Write-Host "##teamcity[blockClosed name='$Server']" + } + } + } + + #region Argument_Building + # NOTE: Using a hashtable allows us to pass a gazillion parameters without worrying about missing one + # or putting them in the wrong order. Plus, until we explicitly make use of it in the scriptblock, it + # does NOTHING. Beautiful. + $serverScriptArguments = @{ + RemoteScriptBlock = $RemoteScriptBlock + AgentScriptBlock = $AgentScriptBlock + ShouldRunScriptBlock = $ShouldRunScriptBlock + ShouldForceRunScriptBlockNumTimes = $ShouldForceRunScriptBlockNumTimes + Arguments = $Arguments + CycleLoadBalancer = $CycleLoadBalancer + CycleOrb = $CycleOrb + CycleOrbLeaveStopped = $CycleOrbLeaveStopped + IgnoreNag = $IgnoreNag + IgnoreWebTests = $IgnoreWebTests + IgnoreTestServers = $IgnoreTestServers + RebootMachine = $RebootMachine + RebootTimeoutMinutes = $RebootTimeoutMinutes + ReRun = $ReRun + MaxReRuns = $MaxReRuns + AwsProfile = $AwsProfile + AwsRegion = $awsRegion + IsRunningInTeamCity = $isRunningInTeamCity + InstanceData = $instanceData + IgnoreWebTestFailures = $ignoreWebTestFailures + IsProductionEnvironment = $isProductionEnvironment + SlackHookUrl = $SlackHookUrl + SlackChannels = $SlackChannels + IsSlackEnabled = $slackEnabled + SlackUsername = $SlackUsername + WebTestFailureMessageLink = $WebTestFailureMessageLink + SitesToSkip = $SitesToSkip + MaximumSitesToTest = $MaximumSitesToTest + SiteThreads = $SiteThreads + WidgetThreads = $WidgetThreads + IgnoreServicesFailures = $IgnoreServicesFailures + } + #endregion Argument_Building + [array]$results = Invoke-Parallel2 -Objects $Servers -NumThreads $ServerParallelism -ThreadPerObject -StopProcessingJobsOnError -Arguments $serverScriptArguments -Script $serverScript + if ((!(Test-IsCollectionNullOrEmpty $results)) -and ($results.Success -contains $false)) { + Write-Error "$loglead : Script block execution failed." + } + + Write-Host "$loglead : Rolling script execution completed in [$($stopwatch.Elapsed)]" +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.ps1 b/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.ps1 new file mode 100644 index 0000000..b860b48 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.ps1 @@ -0,0 +1,182 @@ +function Move-Route53HostedZone { + +<# +.SYNOPSIS + Move a Route53 Hosted Zone to another Route53 Hosted Zone. + +.DESCRIPTION + This function can be used to rename an existing Route53 Hosted Zone in the same account, or to move a Route53 Hosted Zone to a new account. + All record sets will be persisted during the move. + + Note that this function will attempt to register all Alkami VPCs with the newly created zone; to facilitate that process, + call this function only if you have current AWS CLI credentials in each Alkami account. + + Use this function with caution; the source Route53 Hosted Zone WILL BE DELETED during the move. + +.PARAMETER TargetHostedZoneName + [string] The desired name of the new Route53 Hosted Zone (e.g. 'thing.alkami.net'). + +.PARAMETER TargetProfileName + [string] The AWS profile where the Route53 Hosted Zone will be created (e.g. 'temp-prod'). + +.PARAMETER SourceHostedZoneName + [string] The name of the existing Route53 Hosted Zone to relocate (e.g. 'thing.alkami.net'). + +.PARAMETER SourceProfileName + [string] The AWS profile where the existing Route53 Hosted Zone is located (e.g. 'temp-prod'). + +.OUTPUTS + [string] The target Route53 Hosted Zone ID. + +.EXAMPLE + Move-Route53HostedZone -TargetHostedZoneName 'test.alkami.net' -TargetProfileName 'temp-sandbox' -SourceHostedZoneName 'test.alkami.net' -SourceProfileName 'temp-prod' + +[Move-Route53HostedZone] : Looking for pre-existing hosted zone named 'test.alkami.net' in profile 'temp-sandbox'. +[Move-Route53HostedZone] : Looking for source hosted zone 'test.alkami.net' in profile 'temp-prod' +[Move-Route53HostedZone] : Determining baseline target VPC in 'temp-sandbox' +[Move-Route53HostedZone] : Using VPC ID 'vpc-1234' +[Move-Route53HostedZone] : Pulling records from source hosted zone 'test.alkami.net' in profile 'temp-prod'. +[Move-Route53HostedZone] : Creating hosted zone 'test.alkami.net' in profile 'temp-sandbox'. +[Move-Route53HostedZone] : Target hosted zone ID is 'ZONE9999' +[Move-Route53HostedZone] : Copying resource records from source hosted zone to target. +[Move-Route53HostedZone] : Removing resource records from source hosted zone 'test.alkami.net' in profile 'temp-prod'. +[Move-Route53HostedZone] : Removing hosted zone 'test.alkami.net' from profile 'temp-prod' +[Move-Route53HostedZone] : Creating VPC associations for 'ZONE9999'. + +[Move-Route53HostedZone] : Target hosted zone 'test.alkami.net' (zone ID 'ZONE9999') successfully recreated in profile 'temp-sandbox'. +ZONE9999 +#> + + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $TargetHostedZoneName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $TargetProfileName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SourceHostedZoneName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SourceProfileName + ) + + $logLead = (Get-LogLeadName); + $defaultRegion = 'us-east-1' + $sourceRecordSetsToMigrate = $null + + Import-AWSModule + + # Validate that a hosted zone named the specified name does not already exist. + # Amazon is fine with this as long as the VPC associations don't overlap; Alkami is less fine with it . . . + Write-Host "$logLead : Looking for pre-existing hosted zone named '$TargetHostedZoneName' in profile '$TargetProfileName'." + $targetHostedZoneList = Get-R53HostedZonesByName -DNSName $TargetHostedZoneName -ProfileName $TargetProfileName + $targetHostedZone = $targetHostedZoneList | Where-Object { $_.Name -match $TargetHostedZoneName } | Select-Object -First 1 + if ( $null -ne $targetHostedZone ) { + + Write-Error "$logLead : Hosted zone named '$TargetHostedZoneName' already exists using profile '$TargetProfileName'; aborting." + return $targetHostedZone.Id + } + + # Validate that the source hosted zone exists. + Write-Host "$logLead : Looking for source hosted zone '$SourceHostedZoneName' in profile '$SourceProfileName'" + $sourceHostedZoneList = Get-R53HostedZonesByName -DNSName $SourceHostedZoneName -ProfileName $SourceProfileName + $sourceHostedZone = $sourceHostedZoneList | Where-Object { $_.Name -match $SourceHostedZoneName } | Select-Object -First 1 + if ( $null -eq $sourceHostedZone ) { + + # If we didn't find the hosted zone in the source profile, abort with error. + Write-Error "$logLead : Hosted zone named '$SourceHostedZoneName' not found using profile '$SourceProfileName'; aborting." + return $null + } + + $sourceHostedZoneId = $sourceHostedZone.Id + + # Route53 hosted zone creation requires an initial VPC association. Look for one using the target profile. + Write-Host "$logLead : Determining baseline target VPC in '$TargetProfileName'" + $tempVpc = Get-EC2Vpc -ProfileName $TargetProfileName -Region $defaultRegion | Select-Object -First 1 + if ( $null -eq $tempVpc ) { + + Write-Error "$logLead : No VPC found using profile '$TargetProfileName'; a VPC is required for Route53 hosted zone creation." + return $null + } + + # Get record sets to migrate from existing hosted zone. + # Only migrate non-SOA and non-NS records (if any). + Write-Host "$logLead : Pulling records from source hosted zone '$SourceHostedZoneName' in profile '$SourceProfileName'." + $sourceResourceRecordSets = Get-R53ResourceRecordSet -HostedZoneId $sourceHostedZoneId -ProfileName $SourceProfileName + $sourceRecordSetsToMigrate = $sourceResourceRecordSets.ResourceRecordSets | Where-Object { ( $_.Type -ne 'SOA' ) -and ( $_.Type -ne "NS" ) } + + # Create new hosted zone. + Write-Host "$logLead : Creating hosted zone '$TargetHostedZoneName' in profile '$TargetProfileName'." + $targetHostedZone = New-R53HostedZone -Name $TargetHostedZoneName -CallerReference ( Get-Date -Format o ) -ProfileName $TargetProfileName ` + -HostedZoneConfig_PrivateZone $true -VPC_VPCId $tempVpc.VpcId -VPC_VPCRegion $defaultRegion + if ( $null -eq $targetHostedZone ) { + + # Well, we tried . . . + Write-Error "$logLead : Creation of hosted zone '$TargetHostedZoneName' failed." + return $null + } + + Wait-Route53ChangeStatus -ChangeId $targetHostedZone.ChangeInfo.Id -AwsProfileName $TargetProfileName + $targetZoneId = $targetHostedZone.HostedZone.Id + Write-Host "$logLead : Target hosted zone ID is '$targetZoneId'" + + # Add record sets to the newly created hosted zone (if applicable). + if ( $false -eq (Test-IsCollectionNullOrEmpty -collection $sourceRecordSetsToMigrate)) { + + $changeSet = @() + + foreach ( $record in $sourceRecordSetsToMigrate ) { + + $curChange = New-Object Amazon.Route53.Model.Change + $curChange.Action = "CREATE" + $curChange.ResourceRecordSet = $record + + $changeSet += $curChange + } + + Write-Host "$logLead : Copying resource records from source hosted zone to target." + $changeOutput = Edit-R53ResourceRecordSet -HostedZoneId $targetZoneId -ChangeBatch_Comment "Migrating records to new hosted zone" -ChangeBatch_Change $changeSet -ProfileName $TargetProfileName + Wait-Route53ChangeStatus -ChangeId $changeOutput.Id -AwsProfileName $TargetProfileName + } + + # To associate VPCs with the newly created hosted zone, we must first delete the source hosted zone. + # Remove record sets from the source zone prior to deletion. + if ( $false -eq (Test-IsCollectionNullOrEmpty -collection $sourceRecordSetsToMigrate)) { + + $changeSet = @() + + foreach ( $record in $sourceRecordSetsToMigrate ) { + + $curChange = New-Object Amazon.Route53.Model.Change + $curChange.Action = "DELETE" + $curChange.ResourceRecordSet = $record + + $changeSet += $curChange + } + + Write-Host "$logLead : Removing resource records from source hosted zone '$sourceHostedZoneId' in profile '$SourceProfileName'." + $changeOutput = Edit-R53ResourceRecordSet -HostedZoneId $sourceHostedZoneId -ProfileName $SourceProfileName ` + -ChangeBatch_Comment "Removing records for hosted zone migration" ` + -ChangeBatch_Change $changeSet + Wait-Route53ChangeStatus -ChangeId $changeOutput.Id -AwsProfileName $SourceProfileName + } + + Write-Host "$logLead : Removing hosted zone '$sourceHostedZoneId' from profile '$SourceProfileName'" + $changeOutput = Remove-R53HostedZone -Id $sourceHostedZoneId -ProfileName $SourceProfileName -Force + Wait-Route53ChangeStatus -ChangeId $changeOutput.Id -AwsProfileName $SourceProfileName + + # Create the VPC associations. + Write-Host "$logLead : Creating VPC associations for '$targetZoneId'." + Set-Route53HostedZoneVpcAssocations -TargetProfile $TargetProfileName -TargetHostedZoneId $targetZoneId + + # We made it! Let the user know the good news. + Write-Host "$logLead : Target hosted zone '$TargetHostedZoneName' (zone ID '$targetZoneId') successfully recreated in profile '$TargetProfileName'." + return $targetZoneId +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.tests.ps1 new file mode 100644 index 0000000..e0cc1ab --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Move-Route53HostedZone.tests.ps1 @@ -0,0 +1,173 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Move-Route53HostedZone" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { + return @( + @{ 'Region' = 'us-east-1' }, + @{ 'Region' = 'us-west-2' } + ) + } + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Move-Route53HostedZone.tests' } + + Context "Input Validation" { + + It "Target Hosted Zone Name Should Not Be Null" { + + { Move-Route53HostedZone -TargetHostedZoneName $null } | Should -Throw + } + + It "Target Hosted Zone Name Should Not Be Empty" { + + { Move-Route53HostedZone -TargetHostedZoneName '' } | Should -Throw + } + + It "Target Profile Name Should Not Be Null" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test' -TargetProfileName $null } | Should -Throw + } + + It "Target Profile Name Should Not Be Empty" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test' -TargetProfileName '' } | Should -Throw + } + + It "Source Hosted Zone Name Should Not Be Null" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test' -TargetProfileName 'Test' -SourceHostedZoneName $null } | Should -Throw + } + + It "Source Hosted Zone Name Should Not Be Empty" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test' -TargetProfileName 'Test' -SourceHostedZoneName '' } | Should -Throw + } + + It "Source Profile Name Should Not Be Null" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'Test' -SourceHostedZoneName 'test2' -SourceProfileName $null } | Should -Throw + } + + It "Source Profile Name Should Not Be Empty" { + + { Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'Test' -SourceHostedZoneName 'test2' -SourceProfileName '' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Write Error If Target Hosted Zone Already Exists" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'test1'; 'Id' = 'test1' } ) } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'test1' -SourceHostedZoneName 'test2' -SourceProfileName 'test2' + $result | Should -BeExactly 'test1' + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Hosted zone named .* already exists" } + } + + It "Should Write Error If Source Hosted Zone Does Not Exist" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'test3'; 'Id' = 'test3' } ) } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Move-Route53HostedZone -TargetHostedZoneName 'Test1' -TargetProfileName 'Test1' -SourceHostedZoneName 'test2' -SourceProfileName 'test2' + $result | Should -BeNull + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Hosted zone named .* not found" } + } + + It "Should Write Error If No Default VPC Exists" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'test2'; 'Id' = 'test2' } ) } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'test1' -SourceHostedZoneName 'test2' -SourceProfileName 'test2' + $result | Should -BeNull + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 2 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "a VPC is required for Route53 hosted zone creation" } + } + + It "Should Write Error If Hosted Zone Creation Fails" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'test2'; 'Id' = 'test2' } ) } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return @{ 'VpcId' = 'Test' } } + Mock -CommandName New-R53HostedZone -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-R53ResourceRecordSet -ModuleName $moduleForMock -MockWith { return @{ 'ResourceRecordSets' = @() } } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'test1' -SourceHostedZoneName 'test2' -SourceProfileName 'test2' + $result | Should -BeNull + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 2 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-R53ResourceRecordSet -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Creation of hosted zone .* failed." } + } + + It "Should Return Hosted Zone ID on Success" { + + Mock -CommandName New-R53HostedZone -ModuleName $moduleForMock -MockWith { + return @{ + 'ChangeInfo' = @{ 'Id' = 'Test' } + 'HostedZone' = @{ 'Id' = 'SuccessTest' } + } + } + + Mock -CommandName Get-R53ResourceRecordSet -ModuleName $moduleForMock -MockWith { + return @{ + 'ResourceRecordSets' = @( + @{} + ) + } + } + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'test2'; 'Id' = 'test2' } ) } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return @{ 'VpcId' = 'Test1' } } + Mock -CommandName Edit-R53ResourceRecordSet -ModuleName $moduleForMock -MockWith { return @{ 'Id' = 'Test' } } +# Mock -CommandName Unregister-R53VPCFromHostedZone -ModuleName $moduleForMock -MockWith { return @{ 'Id' = 'Test' } } + Mock -CommandName Remove-R53HostedZone -ModuleName $moduleForMock -MockWith { return @{ 'Id' = 'Test' } } + Mock -CommandName Wait-Route53ChangeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-Route53HostedZoneVpcAssocations -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = Move-Route53HostedZone -TargetHostedZoneName 'test1' -TargetProfileName 'test1' -SourceHostedZoneName 'test2' -SourceProfileName 'test2' + $result | Should -BeExactly 'SuccessTest' + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 2 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-R53ResourceRecordSet -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Edit-R53ResourceRecordSet -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Remove-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Wait-Route53ChangeStatus -Times 4 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Set-Route53HostedZoneVpcAssocations -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Unregister-R53VPCFromHostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.ps1 b/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.ps1 new file mode 100644 index 0000000..729add1 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.ps1 @@ -0,0 +1,95 @@ +function New-Route53HostedZone { + +<# +.SYNOPSIS + Create a new Route53 Hosted Zone. + +.DESCRIPTION + Create a new Route53 Hosted Zone. The newly created Route53 Hosted Zone will be empty and ready to populate with records. + + Note that this function will attempt to register all Alkami VPCs with the newly created zone; to facilitate that process, + call this function only if you have current AWS CLI credentials in each Alkami account. + +.PARAMETER HostedZoneName + [string] The desired name of the new Route53 Hosted Zone (e.g. 'thing.alkami.net'). + +.PARAMETER ProfileName + [string] The AWS profile where the Route53 Hosted Zone will be created (e.g. 'temp-prod'). + +.OUTPUTS + [string] The target Route53 Hosted Zone ID. + +.EXAMPLE + New-Route53HostedZone -HostedZoneName 'sandbox-test.alkami.net' -ProfileName 'temp-sandbox' + +[New-Route53HostedZone] : Looking for pre-existing hosted zone named 'sandbox-test.alkami.net' in profile 'temp-sandbox'. +[New-Route53HostedZone] : Determining baseline VPC in 'temp-sandbox' +[New-Route53HostedZone] : Creating hosted zone 'sandbox-test.alkami.net' in profile 'temp-sandbox'. +[Wait-Route53ChangeStatus] : Route53 Change ID '/change/C0629667BL5165ZI58FZ' has status 'INSYNC' after 63.3634024 seconds. +[New-Route53HostedZone] : Hosted Zone ID is '/hostedzone/Z03772702VVYXD5ER64X2' +[New-Route53HostedZone] : Creating VPC associations for '/hostedzone/Z03772702VVYXD5ER64X2'. + +[New-Route53HostedZone] : Hosted zone 'sandbox-test.alkami.net' (zone ID '/hostedzone/Z03772702VVYXD5ER64X2') successfully created in profile 'temp-sandbox'. +/hostedzone/Z03772702VVYXD5ER64X2 +#> + + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $HostedZoneName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName + ) + + Import-AWSModule + + $logLead = (Get-LogLeadName) + $defaultRegion = 'us-east-1' + + # Validate that a hosted zone named the specified name does not already exist. + # Amazon is fine with this as long as the VPC associations don't overlap; Alkami is less fine with it . . . + Write-Host "$logLead : Looking for pre-existing hosted zone named '$HostedZoneName' in profile '$ProfileName'." + $hostedZoneList = Get-R53HostedZonesByName -DNSName $HostedZoneName -ProfileName $ProfileName + $hostedZone = $hostedZoneList | Where-Object { $_.Name -match $HostedZoneName } | Select-Object -First 1 + if ( $null -ne $hostedZone ) { + + Write-Error "$logLead : Hosted zone named '$HostedZoneName' already exists using profile '$ProfileName'; aborting." + return $hostedZone.Id + } + + # Route53 hosted zone creation requires an initial VPC association. Look for one using the provided profile. + Write-Host "$logLead : Determining baseline VPC in '$ProfileName'" + $tempVpc = Get-EC2Vpc -ProfileName $ProfileName -Region $defaultRegion | Select-Object -First 1 + if ( $null -eq $tempVpc ) { + + Write-Error "$logLead : No VPC found using profile '$ProfileName'; a VPC is required for Route53 hosted zone creation." + return $null + } + + # Create new hosted zone. + Write-Host "$logLead : Creating hosted zone '$HostedZoneName' in profile '$ProfileName'." + $hostedZone = New-R53HostedZone -Name $HostedZoneName -CallerReference ( Get-Date -Format o ) -ProfileName $ProfileName ` + -HostedZoneConfig_PrivateZone $true -VPC_VPCId $tempVpc.VpcId -VPC_VPCRegion $defaultRegion + if ( $null -eq $hostedZone ) { + + # Well, we tried . . . + Write-Error "$logLead : Creation of hosted zone '$HostedZoneName' failed." + return $null + } + + Wait-Route53ChangeStatus -ChangeId $hostedZone.ChangeInfo.Id -AwsProfileName $ProfileName + $zoneId = $hostedZone.HostedZone.Id + Write-Host "$logLead : Hosted Zone ID is '$zoneId'" + + # Create the VPC associations. + Write-Host "$logLead : Creating VPC associations for '$zoneId'." + Set-Route53HostedZoneVpcAssocations -TargetProfile $ProfileName -TargetHostedZoneId $zoneId + + # We made it! Let the user know the good news. + Write-Host "$logLead : Hosted zone '$HostedZoneName' (zone ID '$zoneId') successfully created in profile '$ProfileName'." + return $zoneId +} diff --git a/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.tests.ps1 new file mode 100644 index 0000000..ce56ae5 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/New-Route53HostedZone.tests.ps1 @@ -0,0 +1,125 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-Route53HostedZone" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { + return @( + @{ 'Region' = 'us-east-1' }, + @{ 'Region' = 'us-west-2' } + ) + } + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'New-Route53HostedZone.tests' } + + Context "Input Validation" { + + It "Hosted Zone Name Should Not Be Null" { + + { New-Route53HostedZone -HostedZoneName $null } | Should -Throw + } + + It "Hosted Zone Name Should Not Be Empty" { + + { New-Route53HostedZone -HostedZoneName '' } | Should -Throw + } + + It "Profile Name Should Not Be Null" { + + { New-Route53HostedZone -HostedZoneName 'test' -ProfileName $null } | Should -Throw + } + + It "Profile Name Should Not Be Empty" { + + { New-Route53HostedZone -HostedZoneName 'test' -ProfileName '' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Write Error If Hosted Zone Already Exists" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @( @{ 'Name' = 'TestName'; 'Id' = 'TestId' } ) } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = New-Route53HostedZone -HostedZoneName 'TestName' -ProfileName 'Test' + $result | Should -BeExactly 'TestId' + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Hosted zone named .* already exists" } + } + + It "Should Write Error If No Default VPC Exists" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @() } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = New-Route53HostedZone -HostedZoneName 'Test' -ProfileName 'Test' + $result | Should -BeNull + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "a VPC is required for Route53 hosted zone creation" } + } + + It "Should Write Error If Hosted Zone Creation Fails" { + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @() } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return @{ 'VpcId' = 'Test' } } + Mock -CommandName New-R53HostedZone -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = New-Route53HostedZone -HostedZoneName 'Test' -ProfileName 'Test' + $result | Should -BeNull + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Creation of hosted zone .* failed." } + } + + It "Should Return Hosted Zone ID on Success" { + + Mock -CommandName New-R53HostedZone -ModuleName $moduleForMock -MockWith { + return @{ + 'ChangeInfo' = @{ 'Id' = 'Test' } + 'HostedZone' = @{ 'Id' = 'SuccessTest' } + } + } + + Mock -CommandName Get-R53HostedZonesByName -ModuleName $moduleForMock -MockWith { return @() } +# Mock -CommandName Test-VpcExists -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return @{ 'VpcId' = 'Test1' } } +# Mock -CommandName Unregister-R53VPCFromHostedZone -ModuleName $moduleForMock -MockWith { return @{ 'Id' = 'Test' } } + Mock -CommandName Wait-Route53ChangeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-Route53HostedZoneVpcAssocations -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = New-Route53HostedZone -HostedZoneName 'Test' -ProfileName 'Test' + $result | Should -BeExactly 'SuccessTest' + + Assert-MockCalled -CommandName Get-R53HostedZonesByName -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Test-VpcExists -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-R53HostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Wait-Route53ChangeStatus -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Set-Route53HostedZoneVpcAssocations -Times 1 -Exactly -Scope It -ModuleName $moduleForMock +# Assert-MockCalled -CommandName Unregister-R53VPCFromHostedZone -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/New-TcpSocket.ps1 b/Modules/Alkami.DevOps.Operations/Public/New-TcpSocket.ps1 new file mode 100644 index 0000000..ed8643c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/New-TcpSocket.ps1 @@ -0,0 +1,22 @@ +function New-TcpSocket { + <# +.SYNOPSIS + When given an object of type System.Net.IPAddress and , returns System.IO.StreamWriter object. +.EXAMPLE + $socket = New-TcpSocket -IpAddress $IpAddress -Port $port +.INPUTS + IpAddress + Port +.OUTPUTS + System.Net.Sockets.TCPClient +#> + [cmdletbinding()] + param( + $IpAddress, + $Port + ) + + $socket = New-Object System.Net.Sockets.TCPClient + $socket.Connect($IpAddress, $Port) + return $socket +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/New-TcpStreamWriter.ps1 b/Modules/Alkami.DevOps.Operations/Public/New-TcpStreamWriter.ps1 new file mode 100644 index 0000000..cc22304 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/New-TcpStreamWriter.ps1 @@ -0,0 +1,18 @@ +function New-TcpStreamWriter { + <# +.SYNOPSIS + When given an object of type System.Net.Sockets.TCPClient, returns System.IO.StreamWriter object. +.EXAMPLE + $writer = New-TcpStreamWriter -Socket $socket +.INPUTS + Socket +.OUTPUTS + System.IO.StreamWriter +.NOTES + Input Socket object should already be in connected state, will be connected if socket is created by New-TcpSocket. +#> + [cmdletbinding()] + param($Socket) + $Stream = $Socket.GetStream() + return New-Object System.IO.StreamWriter($Stream) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Open-ChocolateyLibDir.ps1 b/Modules/Alkami.DevOps.Operations/Public/Open-ChocolateyLibDir.ps1 new file mode 100644 index 0000000..feb3e22 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Open-ChocolateyLibDir.ps1 @@ -0,0 +1,10 @@ +function Open-ChocolateyLibDir { + <# + .SYNOPSIS + Opens the chocolate lib directory in file explorer. + #> + [CmdletBinding()] + param() + $chocoPath = Join-Path (Get-ChocolateyInstallPath) "lib"; + Invoke-Item $chocoPath; +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Open-HostsFile.ps1 b/Modules/Alkami.DevOps.Operations/Public/Open-HostsFile.ps1 new file mode 100644 index 0000000..562a7fc --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Open-HostsFile.ps1 @@ -0,0 +1,23 @@ +Function Open-HostsFile { +<# +.SYNOPSIS + Opens the Windows hosts file in Notepad++ +#> + [CmdletBinding()] + param( + + ) + + $logLead = Get-LogLeadName + $path = "$($env:windir)\System32\Drivers\etc\hosts" + + if(Test-Path $path) + { + $txtEditorPath = Get-TextEditorPath + Start-Process $txtEditorPath $path + } + else + { + Write-Error "$logLead : The hosts file at '$path' does not exist!"; + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Open-MachineConfig.ps1 b/Modules/Alkami.DevOps.Operations/Public/Open-MachineConfig.ps1 new file mode 100644 index 0000000..e6a17c1 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Open-MachineConfig.ps1 @@ -0,0 +1,24 @@ +Function Open-MachineConfig { +<# +.SYNOPSIS + Opens the .NET machine config in Notepad++ +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [switch] $use32bit + ) + + $logLead = Get-LogLeadName + $path = Get-DotNetConfigPath -use64Bit (!$use32bit.IsPresent) + + if(Test-Path $path) + { + $txtEditorPath = Get-TextEditorPath + Start-Process $txtEditorPath $path + } + else + { + Write-Error "$logLead : The machine.config file at '$path' does not exist!" + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Open-NagConfig.ps1 b/Modules/Alkami.DevOps.Operations/Public/Open-NagConfig.ps1 new file mode 100644 index 0000000..3d8cf9d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Open-NagConfig.ps1 @@ -0,0 +1,21 @@ +Function Open-NagConfig { +<# +.SYNOPSIS + Opens the Nag config file in Notepad++ +#> + [CmdletBinding()] + Param () + + $logLead = Get-LogLeadName + $path = (Join-Path(Get-OrbPath) "Nag\Alkami.App.Nag.Host.Service.exe.config") + + if(Test-Path $path) + { + $txtEditorPath = Get-TextEditorPath + Start-Process $txtEditorPath $path + } + else + { + Write-Error "$logLead : The Nag config file at '$path' does not exist!"; + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Open-PowerShellModulesDir.ps1 b/Modules/Alkami.DevOps.Operations/Public/Open-PowerShellModulesDir.ps1 new file mode 100644 index 0000000..9e7f755 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Open-PowerShellModulesDir.ps1 @@ -0,0 +1,9 @@ +function Open-PowerShellModulesDir { + <# + .SYNOPSIS + Opens the PowerShell modules directory in file explorer. + #> + [CmdletBinding()] + param() + Invoke-Item "C:\Program Files\WindowsPowerShell\Modules"; +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.ps1 b/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.ps1 new file mode 100644 index 0000000..35a6507 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.ps1 @@ -0,0 +1,105 @@ +function Ping-EntrustServices { + +<# +.SYNOPSIS + Warms up Entrust services + +.EXAMPLE + Ping-EntrustServices + [Get-ElbHealthcheckEndpoints] : Name computed as 0-0-Sandbox-entrust-alb + + URL Success Elapsed + --- ------- ------- + https://localhost:8444/identityguardadminservice/services/adminservicev11 True 00:00:00.0414551 + https://localhost:8443/identityguardauthservice/services/authenticationservicev11 True 00:00:00.0410411 + +.PARAMETER skipOutput + Optional flag to suppress output. + +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("NoOutput")] + [switch]$skipOutput + ) + + $logLead = (Get-LogLeadName); + + if ($false -eq (Test-IsEntrustServer)) { + Write-Warning "$logLead : This function is only valid on Entrust servers." + return $null + } + + $maxJobs = 3 + $testPattern = "Entrust IdentityGuard" + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + $endpointsToWarmUp = Get-ElbHealthcheckEndpoints + if (Test-IsCollectionNullOrEmpty -collection $endpointsToWarmUp) { + + Write-Warning "$logLead : Result returned NULL or empty value for endpoints" + return $null + } + + $scriptBlock = { + + param ($endpoint, $arguments) + $testPattern = $arguments[0] + + $scriptResult = New-Object -TypeName PSObject -Property @{ + URL = $endpoint + Success = $false + StatusCode = $null + Elapsed = $null + } + + $stopWatch = New-Object -TypeName System.Diagnostics.Stopwatch + + try { + add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ + + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + $stopWatch.Start() + $response = Invoke-WebRequest -Uri $endpoint -UseBasicParsing + $stopWatch.Stop() + $scriptResult.StatusCode = $response.StatusCode + $scriptResult.Success = ($response.StatusCode -eq 200 -and $response.Content -match $testPattern) + + } catch { + + if ($stopWatch.IsRunning) { + $stopWatch.Stop() + } + } + $scriptResult.Elapsed = $stopWatch.Elapsed.ToString() + + return $scriptResult + } + + $endpointResults = Invoke-Parallel -objects $endpointsToWarmUp -arguments ($testPattern) -script $scriptBlock -numThreads $maxJobs -returnObjects + + $functionStopWatch.Stop() + + if ($skipOutput) { + return $endpointResults + } else { + if ($null -ne ($endpointResults | Where-Object {$_.Success -eq $false})) { + Write-Warning "$logLead : One or more URLs failed the test case:`n" + } + + $endpointResults | Sort-Object -Property Success | Format-Table -Property @{Label="URL";Width=85;e={$_.URL};Alignment="Left"}, @{Label="Success";Width=15;e={$_.Success};Alignment="Right"},@{Label="Elapsed";Width=25;e={$_.Elapsed};Alignment="Right"} | Out-String + Write-Host ("Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString()) + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.tests.ps1 new file mode 100644 index 0000000..34f2f9f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Ping-EntrustServices.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$global:moduleForMock = "" + +Describe "Ping-EntrustServices" { + + + Context "Tier Validation" { + + It "Should Be Run on an Entrust Machine" { + + Mock -CommandName Test-IsEntrustServer -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + $result = ( Ping-EntrustServices ) + + Assert-MockCalled -CommandName Test-IsEntrustServer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "This function is only valid on Entrust servers." } + + $result | Should -BeNullOrEmpty + } + } + + InModuleScope -ModuleName Alkami.DevOps.Operations -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $inScopeModuleForAssert = "Alkami.DevOps.Operations" + + Context "Endpoint Validation" { + + It "Get-ELBHealthcheckEndpoints should not return a null value when Test-IsEntrustServer is true" { + + Mock -CommandName Test-IsEntrustServer -ModuleName $inScopeModuleForAssert -MockWith { return $true } + Mock -CommandName Get-ELBHealthcheckEndpoints -ModuleName $inScopeModuleForAssert -MockWith { return $null } + Mock -CommandName Write-Warning -ModuleName $inScopeModuleForAssert -MockWith {} + + $result = ( Ping-EntrustServices ) + + Assert-MockCalled -CommandName Get-ELBHealthcheckEndpoints -Times 1 -Exactly -Scope It -ModuleName $inScopeModuleForAssert + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $inScopeModuleForAssert -ParameterFilter { $Message -match "Result returned NULL or empty value for endpoints" } + + $result | Should -BeNullOrEmpty + } + + It "Get-ELBHealthcheckEndpoints should not return a empty value when Test-IsEntrustServer is true" { + + Mock -CommandName Test-IsEntrustServer -ModuleName $inScopeModuleForAssert -MockWith { return $true } + Mock -CommandName Get-ELBHealthcheckEndpoints -ModuleName $inScopeModuleForAssert -MockWith { return @() } + Mock -CommandName Write-Warning -ModuleName $inScopeModuleForAssert -MockWith {} + + $result = ( Ping-EntrustServices ) + + Assert-MockCalled -CommandName Get-ELBHealthcheckEndpoints -Times 1 -Exactly -Scope It -ModuleName $inScopeModuleForAssert + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $inScopeModuleForAssert -ParameterFilter { $Message -match "Result returned NULL or empty value for endpoints" } + + $result | Should -BeNullOrEmpty + } + } + + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Ping-Host.ps1 b/Modules/Alkami.DevOps.Operations/Public/Ping-Host.ps1 new file mode 100644 index 0000000..15a9286 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Ping-Host.ps1 @@ -0,0 +1,25 @@ +function Ping-Host { + <# +.SYNOPSIS + Ping a given host with a given timeout + +.PARAMETER HostToPing + What host to ping + +.PARAMETER Timeout + How long to wait for a response +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$HostToPing, + [Parameter(Mandatory = $false)] + [string]$Timeout = 100 + ) + $logLead = Get-LogLeadName + + $ping = New-Object System.Net.NetworkInformation.Ping + $response = $ping.Send($HostToPing, $Timeout) + Write-Verbose "$logLead : Ping result : $($response.Status)" + return $response +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Push-TcpStreamWriterBuffer.ps1 b/Modules/Alkami.DevOps.Operations/Public/Push-TcpStreamWriterBuffer.ps1 new file mode 100644 index 0000000..b386dde --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Push-TcpStreamWriterBuffer.ps1 @@ -0,0 +1,15 @@ +function Push-TcpStreamWriterBuffer { + <# +.SYNOPSIS + Flushes all data in the Stream Writer buffer. When given an object of type System.IO.StreamWriter, executes Flush(). +.EXAMPLE + Push-TcpStreamWriterBuffer -Writer $Writer +.INPUTS + Writer +#> + [cmdletbinding()] + param( + $Writer + ) + $Writer.Flush() +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.ps1 new file mode 100644 index 0000000..1e451d1 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.ps1 @@ -0,0 +1,55 @@ +function Read-ProviderConfigurationFromTenants { + +<# +.SYNOPSIS + Connects to a master database and aggregates configured provider information for all tenants + +.DESCRIPTION + Connects to a master database and aggregates configured provider information for all tenants. Calls Get-ProvidersFromTenantDatabase for + each tenant and returns the unique values from all tenants + +.PARAMETER MasterConnectionString + The full Master databse connection string. Defaults to the local ConnectionString from the machine.config file + +.OUTPUTS + Returns a list of unique providers from all queried databases, with provider type included +#> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Parameter(Mandatory=$false)] + [string]$MasterConnectionString = (Get-ConnectionString "AlkamiMaster") + ) + + $logLead = Get-LogLeadName + + if ([String]::IsNullOrEmpty($MasterConnectionString)) { + + Write-Warning "$logLead : Supply a valid Master database connection string to this function and rerun" + return $null + } + + Write-Verbose "$logLead : Reading provider configuration using master database connection string: [$MasterConnectionString]" + [array]$tenants = Get-FullTenantListFromServer -connectionString $MasterConnectionString + + $validProviderConfiguration = @() + foreach ($tenant in $tenants) { + + Write-Host "$logLead : Reading provider configuration from tenant [$($tenant.Name)]" + [PSObject[]]$providers = Get-ProvidersFromTenantDatabase -TenantConnectionString $tenant.ConnectionString -ValidOnly + + Write-Verbose "$logLead : $($providers.Count) valid providers returned" + + foreach ($provider in $providers) { + + $validProviderConfiguration += New-Object PSObject -Property @{ + + ProviderType = $provider.ProviderType; + ProviderName = $provider.ProviderName; + } + } + } + + return $validProviderConfiguration | Sort-Object -Property ProviderType,ProviderName -Unique +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.tests.ps1 new file mode 100644 index 0000000..9f2db34 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderConfigurationFromTenants.tests.ps1 @@ -0,0 +1,108 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Read-ProviderConfigurationFromTenants" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + Mock -CommandName Get-ProvidersFromTenantDatabase -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-FullTenantListFromServer -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ConnectionString -ModuleName $moduleForMock -MockWith { return "TotesLegitConStr" } + + Context "Tests" { + + It "Writes a Warning and Exits Early if an Empty Master Connection String is Provided" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Read-ProviderConfigurationFromTenants -MasterConnectionString $null | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Supply a valid Master database connection string" } + Assert-MockCalled -CommandName Get-ProvidersFromTenantDatabase -Times 0 -Exactly -Scope It + } + + It "Uses the Parameter Value for the MasterConnectionString to Lookup Tenants" { + + $fakeConnectionString = "Server=localhost;InitialCatalog=LittleBobbyTables;" + Read-ProviderConfigurationFromTenants -MasterConnectionString $fakeConnectionString + Assert-MockCalled -CommandName Get-FullTenantListFromServer -Times 1 -Exactly -Scope It ` + -ParameterFilter { $connectionString -eq $fakeConnectionString } + } + + It "Returns only Unique Provider Data by ProviderType and ProviderName (Single Provider)" { + + Mock -CommandName Get-FullTenantListFromServer -ModuleName $moduleForMock -MockWith { + + return @{ + ConnectionString = "foo"; + Name = "bar"; + } + } + + Mock -CommandName Get-ProvidersFromTenantDatabase -ModuleName $moduleForMock -MockWith { + + $providerA = New-Object PSObject -Property @{ + ProviderType = "Fake"; + ProviderName = "Provider"; + } + + $providerB = New-Object PSObject -Property @{ + ProviderType = "Fake"; + ProviderName = "Provider"; + } + + return @($providerA, $providerB) + } + + [PSObject[]]$results = Read-ProviderConfigurationFromTenants + $results | Should -HaveCount 1 + $results | Should -Not -BeNullOrEmpty + $results.ProviderType | Should -BeExactly "Fake" + $results.ProviderName | Should -BeExactly "Provider" + } + + It "Uses both ProviderName and ProviderType to Determine Provider Uniqueness" { + + Mock -CommandName Get-FullTenantListFromServer -ModuleName $moduleForMock -MockWith { + + return @{ + ConnectionString = "foo"; + Name = "bar"; + } + } + + Mock -CommandName Get-ProvidersFromTenantDatabase -ModuleName $moduleForMock -MockWith { + + + $providerA = New-Object PSObject -Property @{ + ProviderType = "Fake"; + ProviderName = "Provider"; + } + + $providerB = New-Object PSObject -Property @{ + ProviderType = "Real"; + ProviderName = "Provider"; + } + + $providerC = New-Object PSObject -Property @{ + ProviderType = "Real"; + ProviderName = "Talk"; + } + + $providerD = New-Object PSObject -Property @{ + ProviderType = "Fake"; + ProviderName = "Talk"; + } + + return @($providerA, $providerB, $providerC, $providerD) + } + + [PSObject[]]$results = Read-ProviderConfigurationFromTenants + $results | Should -HaveCount 4 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.ps1 new file mode 100644 index 0000000..9981c74 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.ps1 @@ -0,0 +1,49 @@ +function Read-ProviderMappingFile { + +<# +.SYNOPSIS + Parses the Vanguard supplied provider mapping file and returns the results as an object array + +.DESCRIPTION + Parses the Vanguard supplied provider mapping file returned from Get-ProviderMappingFilePath and returns the results as an object array + +.PARAMETER ProviderMappingFilePath + The path to the provider mapping file supplied by Vanguard. Defaults to the result of Get-ProviderMappingFilePath, which returns the file + bundled in the module + +.OUTPUTS + Returns objects hydrated from the JSON file. +#> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Parameter(Mandatory=$false)] + [string]$ProviderMappingFilePath = (Get-ProviderMappingFilePath) + ) + + $logLead = Get-LogLeadName + + if ([String]::IsNullOrEmpty($ProviderMappingFilePath)) { + + Write-Warning "$logLead : Could not find the mapping file at $ProviderMappingFilePath. Execution cannot continue" + return $null + } + + Write-Verbose "$logLead : Reading Provider Mapping File from $ProviderMappingFilePath" + $fileContent = Get-Content -Path $ProviderMappingFilePath + + try { + + [array]$providerObjects = ($fileContent | ConvertFrom-Json).mappings + + } catch { + + Write-Warning "$logLead : An error occurred hydrating objects from file. Check file content and re-run." + Write-Host "$logLead : Error: $($_.Exception.Message)" + return $null + } + + Write-Verbose "$logLead : $($providerObjects.Count) objects hydrated" + return $providerObjects +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.tests.ps1 new file mode 100644 index 0000000..0b22277 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-ProviderMappingFile.tests.ps1 @@ -0,0 +1,45 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Read-ProviderMappingFile" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + + Context "Tests" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + It "Writes a Warning and Returns Null if No File Found" { + + Mock -ModuleName $moduleForMock -CommandName Get-ProviderMappingFilePath -MockWith { return $null } + Read-ProviderMappingFile | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Could not find the mapping file" } + } + + It "Writes a Warning and Returns Null if An Exception Occurs Parsing the File" { + + Mock -ModuleName $moduleForMock -CommandName Get-ProviderMappingFilePath -MockWith { return "Z:\OMG" } + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith { return "thIs-AInT-jsoN-broHeiM" } + + Read-ProviderMappingFile | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "An error occurred hydrating objects from file" } + } + + It "Returns Hydrated Objects" { + + Mock -ModuleName $moduleForMock -CommandName Get-ProviderMappingFilePath -MockWith { return "Z:\OMG" } + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith { return '{"mappings":[{"Leave":"Oops.I.Did.It.Again","Britney":"I.Played.With.Your.Heart","Alone":"Got.Lost.In.The.Game"}]}' } + + $results = Read-ProviderMappingFile + $results | Should -Not -BeNullOrEmpty + $results.Leave | Should -BeExactly "Oops.I.Did.It.Again" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.ps1 new file mode 100644 index 0000000..d8f2ee6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.ps1 @@ -0,0 +1,58 @@ +function Read-RadiumPurgeScript { + + <# + .SYNOPSIS + Returns the full content of the embedded Radium purge T-SQL script + + .DESCRIPTION + Returns the full content of the embedded Radium purge T-SQL script Uses the FileList as defined in + the module PSM1 for location + + .PARAMETER UseJobFilterScriptVariant + When true will return the script variant that filters on job type. This is more impactful to SQL Server and so defaults to false + + .PARAMETER RadiumPurgeScriptPath + When supplied will override the automatically determined script path to the bundled module resources. Any value for UseJobFilterScriptVariant is ignored on this path + + .OUTPUTS + Returns the content of the bundled purge script. + #> + + [CmdletBinding()] + [OutputType([string[]])] + param( + + [Parameter(Mandatory = $false)] + [bool]$UseJobFilterScriptVariant = $false, + + [Parameter(Mandatory = $false)] + [string]$RadiumPurgeScriptPath = "" + ) + + $logLead = Get-LogLeadName + + if (-NOT (Test-StringIsNullOrEmpty -Value $RadiumPurgeScriptPath)) { + + $radiumPurgeScriptFilePath = $RadiumPurgeScriptPath + + } + else { + + if ($UseJobFilterScriptVariant) { + + $radiumPurgeScriptFilePath = $MyInvocation.MyCommand.Module.FileList | Where-Object { $_ -match "RadiumPurgeScriptWithJobFilter.sql" } + + } else { + + $radiumPurgeScriptFilePath = $MyInvocation.MyCommand.Module.FileList | Where-Object { $_ -match "RadiumPurgeScript.sql" } + } + } + + if ((Test-StringIsNullOrEmpty -Value $radiumPurgeScriptFilePath) -or (-NOT (Test-Path -Path $radiumPurgeScriptFilePath))) { + + Write-Warning "$logLead : Could not find bundled purge script. Check the module health or for build problems." + return $null + } + + return (Get-Content -Path $radiumPurgeScriptFilePath -Raw) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.tests.ps1 new file mode 100644 index 0000000..eabfbaa --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Read-RadiumPurgeScript.tests.ps1 @@ -0,0 +1,61 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +# TODO - I can't find a way to effectively mock logic that relies on the module manifest for files. If a way is discovered in the future, add tests here + +Describe "Read-RadiumPurgeScript" { + + # Defining this function due to a "bug" in Pester where calls to the Get-Content mock results in error: "A parameter cannot be found that matches parameter name 'Raw'" + # Presumably because Raw is a Dynamic Parameter + function Get-Content { + + param ( + [Parameter()] + [string]$Path, + [Parameter()] + [switch]$Raw + ) + } + + Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith {} + $fakePath = "Z:\OMG" + + Context "Logic" { + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + + It "Uses the location supplied via argument to locate the purge script" { + + Read-RadiumPurgeScript -RadiumPurgeScriptPath "$fakePath" + Assert-MockCalled -CommandName Get-Content -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Path -eq $fakePath } + } + + It "Ignores the UseJobFilterScriptVariant Argument if a FilePath is provided" { + + Read-RadiumPurgeScript -RadiumPurgeScriptPath "$fakePath" -UseJobFilterScriptVariant $true + Assert-MockCalled -CommandName Get-Content -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Path -eq $fakePath } + } + } + + Context "Error Handling" { + + It "Writes a Warning and Returns Null if No File Found" { + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Read-RadiumPurgeScript | Should -BeNullOrEmpty + Assert-MockCalled -CommandName Get-Content -Times 0 -Exactly -Scope It + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Could not find bundled purge script" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.ps1 b/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.ps1 new file mode 100644 index 0000000..510eb9e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.ps1 @@ -0,0 +1,89 @@ +function Remove-ADUserProfiles { + <# +.SYNOPSIS + Deletes CORP or FH user profiles from the specified system(s). + +.DESCRIPTION + Deletes CORP or FH user profiles from the specified system(s). To dry run the operation, specify the '-WhatIf' parameter. + + To confirm each entry before removal, omit the '-WhatIf' parameter and specify '-Confirm:$true'. + + To act like a mad person and blindly delete any matching user profiles without the opportunity to evaluate each entry, specify + the '-Confirm:$false' parameter or omit that parameter entirely. + NOTE: This is extremely risky and you may cause unknown repercussions. CAVEAT EMPTOR! + +.PARAMETER Servers + The name of the server(s) where the operation should be performed. If omitted, defaults to the current host. + +.PARAMETER SkipSizeCheck + Flag that will skip user profile disk size calculation (which is only provided as a convenience so you know + how much disk space you recovered from the operation). Passing this flag makes the execution faster, but you + get less information as a trade-off. Choose wisely. + +.PARAMETER Domains + Array of domains to process. If omitted, defaults to both CORP and FH. +#> + [CmdLetBinding( + SupportsShouldProcess, + ConfirmImpact = 'Low' + )] + param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]] $Servers = @(Get-FullyQualifiedServerName), + + [Parameter(Mandatory = $false)] + [switch] $SkipSizeCheck, + + [Parameter(Mandatory = $false)] + [ValidateSet('CORP', 'FH')] + [string[]] $Domains = @('CORP', 'FH') + ) + + $logLead = Get-LogLeadName + + foreach ( $server in $Servers ) { + + Write-Host "$logLead : Processing server $server" + + # Determine if we are executing remotely. + $splatParams = @{} + if ( -not (Compare-StringToLocalMachineIdentifiers -stringToCheck $server )) { + + $splatParams['ComputerName'] = "$server" + } + + $totalMb = 0 + $toProcess = Get-ADUserProfileListToRemove -ComputerName $server -Domains $Domains | Sort-Object LocalPath + + # Phase 1: Always print out what we would do, whether the user is in dry run mode or not. + foreach ( $entry in $toProcess ) { + + Write-Warning "$logLead : Identified user profile removal candidate '$($entry.LocalPath)'." + } + + # Phase 2: Run the actual delete logic (as long as we aren't in dry run mode). + if ( -not $WhatIfPreference ) { + foreach ( $entry in $toProcess ) { + + $profilePath = $entry.LocalPath + if ($PSCmdlet.ShouldProcess($profilePath, 'Remove User Profile')) { + + if ( -not $SkipSizeCheck ) { + + $mbSize = Get-FolderSizeMb -Path $profilePath -ComputerName $server + Write-Host "$logLead : Removal of $profilePath frees up $mbSize Mbs." + $totalMb += $mbSize + } + + Remove-CimInstance -InputObject $entry @splatParams + } + } + + if ( -not $SkipSizeCheck ) { + + Write-Host "$logLead : Total filesystem Mbs freed = $totalMb." + } + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.tests.ps1 new file mode 100644 index 0000000..4ed241b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Remove-ADUserProfiles.tests.ps1 @@ -0,0 +1,139 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" + +InModuleScope -ModuleName Alkami.DevOps.Operations -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)" + Import-Module $global:functionPath -Force + $moduleForMock = '' + $inScopeModuleForAssert = 'Alkami.DevOps.Operations' + + Describe 'Remove-ADUserProfiles' { + + $testServer = 'server1.test.local' + + $testUser1 = [Microsoft.Management.Infrastructure.CimInstance]::new('Win32_UserProfile') + $testUser1.CimInstanceProperties.Add([Microsoft.Management.Infrastructure.CimProperty]::Create('LocalPath', 'TestDrive:\Test\test1', [cimtype]::String, 'Property, ReadOnly')) + $testUser2 = [Microsoft.Management.Infrastructure.CimInstance]::new('Win32_UserProfile') + $testUser2.CimInstanceProperties.Add([Microsoft.Management.Infrastructure.CimProperty]::Create('LocalPath', 'TestDrive:\Test\test2', [cimtype]::String, 'Property, ReadOnly')) + $testUsers = @( $testUser1, $testUser2) + + Mock -CommandName Get-FullyQualifiedServerName -ModuleName $moduleForMock -MockWith { return $testServer } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Remove-ADUserProfiles.test' } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-FolderSizeMb -ModuleName $moduleForMock -MockWith { return 1 } + Mock -CommandName Remove-CimInstance -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ADUserProfileListToRemove -ModuleName $moduleForMock -MockWith { return $testUsers } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Context 'Input Validation' { + + It 'Throws if Servers is null' { + + { Remove-ADUserProfiles -Servers $null } | Should -Throw + } + + It 'Throws if Servers is empty' { + + { Remove-ADUserProfiles -Servers @() } | Should -Throw + } + + It 'Throws if Domain list contains invalid value' { + + { Remove-ADUserProfiles -Domains @('Test') } | Should -Throw + } + } + + Context 'Logic' { + + It 'Performs command on localhost by default' { + + Remove-ADUserProfiles -WhatIf | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-FullyQualifiedServerName -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileListToRemove -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $testServer } + } + + It 'Performs command on localhost if Servers array contains current server' { + + Remove-ADUserProfiles -Servers @($testServer) -WhatIf | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-FullyQualifiedServerName -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileListToRemove -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $testServer } + } + + It 'Performs command on all remote servers if Servers array does not contain current server' { + + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $false } + + $testServers = @( 'server2.test.local', 'server3.test.local' ) + + Remove-ADUserProfiles -Servers $testServers -WhatIf | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-FullyQualifiedServerName -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Compare-StringToLocalMachineIdentifiers -Times 2 -Exactly -Scope It ` + -ParameterFilter { $testServers -contains $stringToCheck } + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileListToRemove -Times 2 -Exactly -Scope It ` + -ParameterFilter { $testServers -contains $ComputerName } + + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Writes warning and aborts if WhatIf flag is present' { + + Remove-ADUserProfiles -WhatIf | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Remove-CimInstance -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Write-Warning -Times $testUsers.Length -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Identified user profile removal candidate' } + } + + It 'Writes warning and continues if WhatIf flag is not present' { + + Remove-ADUserProfiles -Confirm:$false | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Remove-CimInstance -Times 1 -Scope It + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Write-Warning -Times $testUsers.Length -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Identified user profile removal candidate' } + } + + It 'Uses Domain parameter if provided' { + + $test = @('CORP') + Remove-ADUserProfiles -Confirm:$false -Domains $test | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-ADUserProfileListToRemove -Times 1 -Exactly -Scope It ` + -ParameterFilter { $null -eq (Compare-Object $Domains $test ) } + } + + It 'Removes user profiles' { + + Remove-ADUserProfiles -Confirm:$false | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Remove-CimInstance -Times $testUsers.Length -Exactly -Scope It + } + + It 'Calculates size of profile directories by default' { + + Remove-ADUserProfiles -Confirm:$false | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-FolderSizeMb -Times $testUsers.Length -Exactly -Scope It + } + + It 'Does not calculate size of profile directories if SkipSizeCheck is present' { + + Remove-ADUserProfiles -Confirm:$false -SkipSizeCheck | Out-Null + + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-FolderSizeMb -Times 0 -Exactly -Scope It + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Repair-LoadBalancerState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Repair-LoadBalancerState.ps1 new file mode 100644 index 0000000..6880a18 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Repair-LoadBalancerState.ps1 @@ -0,0 +1,56 @@ +function Repair-LoadBalancerState { +<# +.SYNOPSIS + This is used to look to see if a server is on per EC2, then to ensure the loadbalancer state matches the on/off status of the machine. + This does not try to see if any site or server functionality is available, it just resets the states. + +.PARAMETER ServerList + [string[]] This is a list of servernames to connect to for resetting + +.PARAMETER ProfileName + [string] AWS Profile name + +.PARAMETER Region + [string] AWS Region +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string[]]$ServerList, + [string]$ProfileName, + [string]$Region + ) + + # if there are no servers, skip it! + if(Test-IsCollectionNullOrEmpty $ServerList) + { + Write-Host "No servers to act on. Skipping step." + return + } + + #Loop over the servers and set Load Balancer to reflect current on status of machine. + $sb = { + param ($hostname, $arguments) + + $loadbalancerScript = { + param($sb_Hostname, $sb_state, $sb_ProfileName, $sb_Region) + Set-LoadBalancerState -server $sb_Hostname -DesiredState $sb_state -AwsProfileName $sb_ProfileName -AwsRegion $sb_Region + } + + $isServerOn = (Test-ComputerIsAvailable -ComputerName $hostname) + $state = 'down' + if ($isServerOn) { + $state = 'up' + } + + try { + Invoke-CommandWithRetry -Arguments ($hostname, $state, $arguments.ProfileName, $arguments.Region) -MaxRetries 3 -Exponential -ScriptBlock $loadbalancerScript + Write-Host "Success. $hostname is back in rotation." + } catch { + Write-Host "An Error occurred while putting $hostname into rotation" + Write-Warning $_ + } + } + + (Invoke-Parallel2 -Objects $ServerList -Arguments @{ProfileName = $ProfileName; Region = $Region;} -Script $sb) | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.ps1 b/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.ps1 new file mode 100644 index 0000000..264c45a --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.ps1 @@ -0,0 +1,92 @@ +function Restart-Tier { + + <# + .SYNOPSIS + Restarts IIS and Alkami Windows Services. Archives Alkami and Chocolatey logs on disk and clears .NET temporary files + + .DESCRIPTION + Restarts IIS and Alkami Windows Services. Archives Alkami and Chocolatey logs on disk and clears .NET temporary files. + Max service start parallelism defaults to 10 and can be set by specifying a positive int for the parameter maxParallel. + Admin websites will be opened in the user's default browser if -openAdmin is specified + + .PARAMETER openAdmin + Opens admin websites in the users default browser prior to running site pinger. Only valid on Web servers + + .PARAMETER maxParallel + The maximum number of services to start in parallel. Defaults to 10 + + .PARAMETER ArchiveLogFiles + Enable archiving of logfiles to zip files. By default, we do not Archive Log Files and only delete current logs + and expired archives. + + .EXAMPLE + Restart-Tier -maxParallel 2 + +[Stop-IISOnly] : Stopping IIS... +[Stop-IISOnly] : Done +[Stop-IISAndServices] : Sleeping for 5 seconds... +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 1 chocolatey services. +Stopping Service AlkamiOnboardEventBroker +.. +[Stop-ProcessIfFound] : Stopping 1 instance(s) of process SMSvcHost +[Remove-DotNetTemporaryFiles] : Deleting ASP.NET Temporary files +[Backup-ORBLogFiles] : Backing up *.log* files to C:\ORBLogs\Archive +Compressing Files +C:\ORBLogs\20170725041214\Alkami.App.Audit.Host.log +C:\ORBLogs\20170725042246\Alkami.App.Audit.Host.log +[Backup-ORBLogFiles] : Backup Complete +[Remove-OldArchivedLogFiles] : Looking for old files to delete +[Remove-OldArchivedLogFiles] : Looking for empty folders to delete +[Backup-LogFiles] : Backing up / Archiving logs in "C:\ProgramData\chocolatey\logs" +[Backup-LogFiles] : Backup complete. +[Remove-OldArchivedLogFiles] : Looking for old files to delete +[Remove-OldArchivedLogFiles] : Looking for empty folders to delete +[Start-IISOnly] : Starting IIS... +[Start-IISOnly] : Done +[Start-IISAndServices] : Sleeping for 5 seconds... +Starting Service AlkamiOnboardEventBroker +. +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 1 chocolatey services. +[Start-IISAndServices] : No Services Found to Start +[Ping-AlkamiWebSites] : Starting Site Warmup + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [switch]$openAdmin, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$maxParallel = 10, + + [Parameter(Mandatory = $false)] + [switch]$ArchiveLogFiles + ) + + $logLead = (Get-LogLeadName); + + Stop-IISAndServices + Move-LogsAndDeleteDotNetTemps -ArchiveLogFiles:$ArchiveLogFiles + Start-IISAndServices -maxParallel $maxParallel + + if (Test-IsWebServer) { + + if ( $openAdmin ) { + + Write-Verbose "$logLead : Opening Admin Sites in Default Browser" + Open-AlkamiSites -adminSitesOnly + } + + Ping-AlkamiWebSites + + } elseif (Test-IsAppServer) { + + Ping-AlkamiServices + } else { + + Write-Warning "$logLead : Could Not Determine Tier. No Service Warmup Performed" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.tests.ps1 new file mode 100644 index 0000000..312cef3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Restart-Tier.tests.ps1 @@ -0,0 +1,119 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Restart-Tier" { + + Mock -ModuleName $moduleForMock -CommandName Stop-IISAndServices -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Move-LogsAndDeleteDotNetTemps -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-IISAndServices -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Open-AlkamiSites -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Ping-AlkamiWebSites -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Ping-AlkamiServices -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Context "Parameter Validation" { + + It "Defaults to 10 Parallel Services" { + + Restart-Tier + Assert-MockCalled -CommandName Start-IISAndServices -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $maxParallel -eq 10 } + } + + It "Uses the Supplied MaxParallel Parameter in the Call to Start-IISAndServices" { + + Restart-Tier -maxParallel 5 + Assert-MockCalled -CommandName Start-IISAndServices -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $maxParallel -eq 5 } + } + + It "Defaults to Not Opening Websites" { + + Restart-Tier + Assert-MockCalled -CommandName Open-AlkamiSites -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Calls Open-AlkamiSites When -openAdmin is Supplied And Test-IsWebServer is True" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $true } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Open-AlkamiSites -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $adminSitesOnly.IsPresent } + } + + It "Does Not Call Open-AlkamiSites When -openAdmin is Supplied And Test-IsWebServer is False" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $false } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Open-AlkamiSites -Times 0 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $adminSitesOnly.IsPresent } + } + + It "Defaults to skipping the zipping" { + Restart-Tier + Assert-MockCalled -CommandName Move-LogsAndDeleteDotNetTemps -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter {$ArchiveLogFiles -eq $false} + } + + It "Passes the switch to zip log files" { + Restart-Tier -ArchiveLogFiles + Assert-MockCalled -CommandName Move-LogsAndDeleteDotNetTemps -ModuleName $moduleForMock -Times 1 -Exactly -Scope It -ParameterFilter {$ArchiveLogFiles -eq $true} + } + } + + Context "Tier Handling" { + + It "Calls Ping-AlkamiServices When Test-IsAppServer is True" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $false} + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $true } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Ping-AlkamiServices -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Does Not Call Ping-AlkamiServices When Test-IsAppServer is False" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Ping-AlkamiServices -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Calls Ping-AlkamiWebSites When Test-IsWebServer is True and Open-AlkamiSites is Not Passed" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + Restart-Tier + Assert-MockCalled -CommandName Ping-AlkamiWebSites -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Calls Ping-AlkamiWebSites When Test-IsWebServer is True and Open-AlkamiSites is Passed" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Ping-AlkamiWebSites -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Does Not Call Ping-AlkamiWebSites When Test-IsWebServer is False" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + Restart-Tier -openAdmin + Assert-MockCalled -CommandName Ping-AlkamiWebSites -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Writes a Warning When Both Test-IsWebServer and Test-IsAppServer are False" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + Restart-Tier + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Could Not Determine Tier" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Select-RunningEC2InstancesByHostname.ps1 b/Modules/Alkami.DevOps.Operations/Public/Select-RunningEC2InstancesByHostname.ps1 new file mode 100644 index 0000000..3f5fd16 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Select-RunningEC2InstancesByHostname.ps1 @@ -0,0 +1,35 @@ +function Select-RunningEC2InstancesByHostname { + <# + .SYNOPSIS + Filters the list of Servers by which servers are running in AWS EC2. + + .PARAMETER Servers + The server list of hostnames to filter by. + + .PARAMETER ProfileName + The AWS profile name to use when querying for EC2 instances. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + $Servers, + [Parameter(Mandatory=$true)] + $ProfileName + ) + + [array]$instances = Get-EC2InstancesByHostname -Servers $Servers -ProfileName $ProfileName + + # Only return the servers that are in a running state. + $runningServers = @() + $counter = 0 + foreach($server in $Servers) { + $instance = $instances[$counter] + if($instance.State.Name -eq "running") { + $runningServers += $server + } else { + Write-Warning "$loglead : Machine $server is not running." + } + $counter++ + } + return $runningServers +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Counter.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Counter.Tests.ps1 new file mode 100644 index 0000000..ce405e3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Counter.Tests.ps1 @@ -0,0 +1,66 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Send-Counter" { + Mock -CommandName Send-Metric -MockWith { return $null } -ModuleName $moduleForMock + Mock -commandName Write-Verbose -MockWith { return $null } + + Context "Input Validation" { + + It "MetricName Cannot be empty" { + { Send-Counter -MetricName '' } | Should -Throw + } + It "Count Cannot be empty" { + { Send-Counter -count ' ' } | Should -Throw + } + } + Context "Param Passing is honored" { + + It "Supplied Ip is passed to Send-Metric" { + Send-Counter -MetricName "bork" -count 8 -Ip "666.666.666.666" + Assert-MockCalled Send-Metric -ParameterFilter { $Ip -eq "666.666.666.666" } -ModuleName $moduleForMock + } + It "Supplied Port is passed to Send-Metric" { + Send-Counter -MetricName "bork" -count 8 -Port 666 -verbose + Assert-MockCalled Send-Metric -ParameterFilter { $Port -eq "666" } -ModuleName $moduleForMock + } + It "Supplied StatsdUrl is passed to Send-Metric" { + Send-Counter -MetricName "bork" -count 8 -StatsdUrl "fake.fake.fake" + Assert-MockCalled Send-Metric -ParameterFilter { $StatsdUrl -eq "fake.fake.fake" } -ModuleName $moduleForMock + } + } + Context "Metric String Concat" { + + It "Metric is concatenated correctly and sent to Send-Metric" { + Send-Counter -MetricName "bork" -count 8 + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:8|c" } -ModuleName $moduleForMock + } + } + Context "Metric Counter" { + + It "accurate increment value is sent to Send-Counter" { + Send-Counter -MetricName "bork" -count 1 + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|c" } -ModuleName $moduleForMock + } + It "accurate increment string value is sent to Send-Counter" { + Send-Counter -MetricName "bork" -count "1" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|c" } -ModuleName $moduleForMock + } + It "accurate decrement value is sent to Send-Counter" { + Send-Counter -MetricName "bork" -count -1 + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|c" } -ModuleName $moduleForMock + } + It "accurate decrement string value is sent to Send-Counter" { + Send-Counter -MetricName "bork" -count "-1" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|c" } -ModuleName $moduleForMock + } + It "suppling string as count throws" { + { Send-Counter -MetricName "bork" -count "fake" } | Should -Throw + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Counter.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Counter.ps1 new file mode 100644 index 0000000..75b5aa7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Counter.ps1 @@ -0,0 +1,47 @@ +function Send-Counter { + <# +.SYNOPSIS + Sends a Counter Metric to StatsD service. +.EXAMPLE + Send-Counter -MetricName "bork" -Count 8 -verbose +.INPUTS + Metric: namespace of metric + Count: amount to increment metric + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $MetricName, + [ValidateNotNullOrEmpty()] + [int]$Count, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + [string]$metric = $MetricName + ":" + "$Count" + "|c" + + Write-Verbose "$logLead : Metric:$metric" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + Write-Verbose "StatsdUrl:$StatsdUrl" + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + Write-Verbose "Port:$Port" + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + Write-Verbose "Ip:$Ip" + $params += @{'Ip' = $Ip } + } + + Send-Metric -Metric $metric @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.Tests.ps1 new file mode 100644 index 0000000..7a1e92b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.Tests.ps1 @@ -0,0 +1,106 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Send-DeploymentPackageCounter" { + Mock -CommandName Send-Counter -MockWith { return $null } -ModuleName $moduleForMock + Mock -commandName Write-Verbose -MockWith { return $null } + + Context "Input Validation" { + + It "Metric Param Cannot be empty" { + + { Send-DeploymentPackageCounter -metric '' } | Should -Throw + } + It "EnvType Param Cannot be empty" { + + { Send-DeploymentPackageCounter -EnvType '' } | Should -Throw + } + It "Designation Param Cannot be empty" { + + { Send-DeploymentPackageCounter -Designation '' } | Should -Throw + } + It "PackageName Param Cannot be empty" { + + { Send-DeploymentPackageCounter -PackageName '' } | Should -Throw + } + It "Count Param Cannot be empty" { + + { Send-DeploymentPackageCounter -Count ' ' } | Should -Throw + } + } + Context "Metric Counter" { + + It "accurate increment value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count 1 -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "1" } -ModuleName $moduleForMock + } + It "accurate increment string value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "10" -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "10" } -ModuleName $moduleForMock + } + It "accurate decrement value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count -1 -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "-1" } -ModuleName $moduleForMock + } + It "accurate decrement string value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "-10" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "-10" } -ModuleName $moduleForMock + } + It "suppling string as count throws " { + { Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "fake" } | Should -Throw + } + It "99 string value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "99" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "99" } -ModuleName $moduleForMock + } + It "1f should not be valid count" { + { Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "1F" } | Should -Throw + + } + It "-999 decrement string value is sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "-999" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "-999" } -ModuleName $moduleForMock + } + It "100,000 should be valid count" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "100,000" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "100,000" } -ModuleName $moduleForMock + } + It "0.000000009 should not be valid count" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "0.000000000" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "0.000000000" } -ModuleName $moduleForMock + } + It "10:00 should not be valid count" { + { Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "10:00" } | Should -Throw + } + It "z23 should not be valid count" { + { Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count "z23" } | Should -Throw + } + } + Context "Param Passing is honored" { + + It "Supplied Ip is passed to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count 1 -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Ip -eq "666.666.666.666" } -ModuleName $moduleForMock + } + It "Supplied Port is passed to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count 1 -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Port -eq "666" } -ModuleName $moduleForMock + } + It "Supplied StatsdUrl is passed to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count 1 -StatsdUrl "fake.fake.fake" + Assert-MockCalled Send-Counter -ParameterFilter { $StatsdUrl -eq "fake.fake.fake" } -ModuleName $moduleForMock + } + } + Context "Metric String Concat" { + + It "Designation Metric is concatenated correctly and sent to Send-Counter" { + Send-DeploymentPackageCounter -EnvType "fake" -Designation "fake" -PackageName "orb" -Count 1 + Assert-MockCalled Send-Counter -ParameterFilter { $MetricName -eq "fake.fake.pipeline.deployment.package.count.orb" } -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.ps1 new file mode 100644 index 0000000..13d3362 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageCounter.ps1 @@ -0,0 +1,51 @@ +function Send-DeploymentPackageCounter { + <# +.SYNOPSIS + increments or decrements a designation's deployment package count counter +.EXAMPLE + Send-DeploymentPackageCounter -EnvType "dev" -Designation "ci1" -DeploymenType "orb" -Count 1 -Verbose + Send-DeploymentPackageCounter -EnvType "dev" -Designation "ci1" -DeploymenType "Alkami.MicroService.fake.service.host" -Count 1 -Verbose +.INPUTS + PackageName: namespace of metric + Count: amount to increment metric + EnvType: pull from machine.config "Environment.UserPrefix" + Designation: pull from machine.config "Environment.NameSafeDesignation" + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $EnvType, + [ValidateNotNullOrEmpty()] + $Designation, + [ValidateNotNullOrEmpty()] + $PackageName, + [ValidateNotNullOrEmpty()] + [int]$Count = 1, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + $deploymentNamespace = "pipeline.deployment.package.count" + [string]$designationMetricName = "$EnvType.$Designation.$deploymentNamespace.$PackageName" + + Write-Verbose "$logLead : MetricName:$designationMetricName Count:$Count" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + $params += @{'Ip' = $Ip } + } + + Send-Counter -MetricName $designationMetricName -Count $Count @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageSizeOrbGauge.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageSizeOrbGauge.ps1 new file mode 100644 index 0000000..598afd3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentPackageSizeOrbGauge.ps1 @@ -0,0 +1,44 @@ +function Send-DeploymentPackageSizeOrbGauge { + <# +.SYNOPSIS + Sends the size of the orb binary as a metric +.EXAMPLE + Send-DeploymentPackageSizeOrbGauge -Release "Release.2020.1.1" -Gauge 11111111111 -verbose +.INPUTS + Release: orb verison + Gauge: value of orb size, in bytes + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $Release, + [ValidateNotNullOrEmpty()] + [int]$Gauge, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + $deploymentNamespace = "size.orb" + [string]$designationMetricName = "$deploymentNamespace.$Release" + + Write-Verbose "$logLead : MetricName:$designationMetricName Gauge:$Gauge" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + $params += @{'Ip' = $Ip } + } + + Send-Gauge -MetricName $designationMetricName -Gauge $Gauge @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.Tests.ps1 new file mode 100644 index 0000000..855bf6c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.Tests.ps1 @@ -0,0 +1,79 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Send-DeploymentTypeCounter" { + Mock -CommandName Send-Counter -MockWith { return $null } -ModuleName $moduleForMock + Mock -commandName Write-Verbose -MockWith { return $null } + + Context "Input Validation" { + + It "Metric Param Cannot be empty" { + + { Send-DeploymentTypeCounter -metric '' } | Should -Throw + } + It "EnvType Param Cannot be empty" { + + { Send-DeploymentTypeCounter -EnvType '' } | Should -Throw + } + It "Designation Param Cannot be empty" { + + { Send-DeploymentTypeCounter -Designation '' } | Should -Throw + } + It "DeploymentType Param Cannot be empty" { + + { Send-DeploymentTypeCounter -DeploymentType '' } | Should -Throw + } + It "Count Param Cannot be empty" { + { Send-DeploymentTypeCounter -Count '' } | Should -Throw + } + } + Context "Metric Counter" { + + It "accurate increment value is sent to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count 1 -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "1" } -ModuleName $moduleForMock + } + It "accurate increment string value is sent to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count "10" -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "10" } -ModuleName $moduleForMock + } + It "accurate decrement value is sent to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count -1 -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "-1" } -ModuleName $moduleForMock + } + It "accurate decrement string value is sent to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count "-10" -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Count -eq "-10" } -ModuleName $moduleForMock + } + It "suppling string as count throws " { + { Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count "fake" } | Should -Throw + } + } + Context "Param Passing is honored" { + + It "Supplied Ip is passed to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count 1 -Ip "666.666.666.666" + Assert-MockCalled Send-Counter -ParameterFilter { $Ip -eq "666.666.666.666" } -ModuleName $moduleForMock + } + It "Supplied Port is passed to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count 1 -Port "666" + Assert-MockCalled Send-Counter -ParameterFilter { $Port -eq "666" } -ModuleName $moduleForMock + } + It "Supplied StatsdUrl is passed to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count 1 -StatsdUrl "fake.fake.fake" + Assert-MockCalled Send-Counter -ParameterFilter { $StatsdUrl -eq "fake.fake.fake" } -ModuleName $moduleForMock + } + } + Context "Metric String Concat" { + + It "Designation Metric is concatenated correctly and sent to Send-Counter" { + Send-DeploymentTypeCounter -EnvType "fake" -Designation "fake" -DeploymentType "downtime" -Count 1 + Assert-MockCalled Send-Counter -ParameterFilter { $MetricName -eq "fake.fake.pipeline.deployment.type.downtime" } -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.ps1 new file mode 100644 index 0000000..18e65f6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeCounter.ps1 @@ -0,0 +1,50 @@ +function Send-DeploymentTypeCounter { + <# +.SYNOPSIS + increments or decrements a designation's deployment type counter +.EXAMPLE + Send-DeploymentTypeCounter -EnvType "dev" -Designation "ci1" -DeploymentType "downtime" -Count 1 -Verbose +.INPUTS + DeploymentType: namespace of metric + Count: amount to increment metric + EnvType: pull from machine.config "Environment.UserPrefix" + Designation: pull from machine.config "Environment.NameSafeDesignation" + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $EnvType, + [ValidateNotNullOrEmpty()] + $Designation, + [ValidateNotNullOrEmpty()] + $DeploymentType, + [ValidatePattern("\d")] # maybe make this just non-zero integers ? + $Count, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + $deploymentNamespace = "pipeline.deployment.type" + [string]$designationMetricName = "$EnvType.$Designation.$deploymentNamespace.$DeploymentType" + + Write-Verbose "$logLead : MetricName:$metricName Count:$Count" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + $params += @{'Ip' = $Ip } + } + + Send-Counter -MetricName $designationMetricName -Count $Count @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeTimer.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeTimer.ps1 new file mode 100644 index 0000000..3f5f315 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-DeploymentTypeTimer.ps1 @@ -0,0 +1,50 @@ +function Send-DeploymentTypeTimer { + <# +.SYNOPSIS + Sends the amount of time taken for a deployment type +.EXAMPLE + Send-DeploymentTypeTimer -EnvType "dev" -Designation "ci1" -DeploymentType "downtime" -Time 1 -Verbose +.INPUTS + DeploymenType: namespace of metric + Time: value in Milliseconds + EnvType: pull from machine.config "Environment.UserPrefix" + Designation: pull from machine.config "Environment.NameSafeDesignation" + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $EnvType, + [ValidateNotNullOrEmpty()] + $Designation, + [ValidateNotNullOrEmpty()] + $DeploymentType, + [ValidatePattern("\d")] # maybe make this just non-zero integers ? + $Time, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + $deploymentNamespace = "pipeline.deployment.time" + [string]$designationMetricName = "$EnvType.$Designation.$deploymentNamespace.$DeploymentType" + + Write-Verbose "$logLead : designationMetricName:$designationMetricName Time:$Time" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + $params += @{'Ip' = $Ip } + } + + Send-Timer -MetricName $designationMetricName -Time $Time @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Gauge.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Gauge.ps1 new file mode 100644 index 0000000..a84e5e0 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Gauge.ps1 @@ -0,0 +1,47 @@ +function Send-Gauge { + <# +.SYNOPSIS + Sends a Gauge Metric to StatsD service. +.EXAMPLE + Send-Gauge -MetricName "bork" -Gauge 8 -verbose +.INPUTS + Metric: string metric name + Gauge: value of metric + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port in Send-Metric +#> + + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $MetricName, + [ValidateNotNullOrEmpty()] + [int]$Gauge, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + [string]$metric = $MetricName + ":" + "$Gauge" + "|g" + + Write-Verbose "$logLead : Metric:$metric" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + Write-Verbose "StatsdUrl:$StatsdUrl" + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + Write-Verbose "Port:$Port" + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + Write-Verbose "Ip:$Ip" + $params += @{'Ip' = $Ip } + } + + Send-Metric -Metric $metric @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-IncidentSnsMessage.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-IncidentSnsMessage.ps1 new file mode 100644 index 0000000..f242c76 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-IncidentSnsMessage.ps1 @@ -0,0 +1,198 @@ +Function Send-IncidentSnsMessage { + <# + .SYNOPSIS + Sends an SNS message to create a new StatusPage.Io Incident. + + .PARAMETER Message + Message to package in the SNS Message. Not especially important to the automation, but useful for human reading and logging. + + .PARAMETER StatusfactionId + Part of the Dynamo PK. Guid. Can be supplied when creating an incident. Required when updating incidents. + + .PARAMETER Subject + Used for determining what to do with the message. + + .PARAMETER TopicArn + Arn of the SNS topic to send the message to. + + .PARAMETER ProfileName + AWS Creds. + + .PARAMETER Region + Aws Region. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseLiteralInitializerForHashtable', '', Scope='Function', Justification='.Net hashtables are case sensitive by default, and we need a .Net hashtable here.')] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Incident, + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [string]$StatusfactionId, + [Parameter(Mandatory = $true)] + [string]$Subject, + [Parameter(Mandatory = $true)] + [string]$TopicArn, + [Parameter(Mandatory = $false)] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$Region + ) + $logLead = (Get-LogLeadName) + + Import-AWSModule + + #Region Dynamo + if($Subject -eq "CreateIncident") + { + Write-Verbose "$logLead : Building Dynamo Item" + # Build Item to put into dynamo. + # If a guid was supplied, use that. Otherwise, create one. + if([string]::IsNullOrEmpty($statusfactionId)){ + $statusfactionId = New-Guid + } + + # Build TTL value. + $ttl = (Get-Date).ToUniversalTime().AddDays(45) + $unixTtl = [int64](Get-Date($ttl) -UFormat %s) + + $item = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,Amazon.DynamoDBv2.Model.AttributeValue]' + $item.Add("Environment", ($Incident.ComponentName | AsDynamoMessageValue)) + $item.Add("Id", ($statusfactionId | AsDynamoMessageValue)) + $item.Add("Origin", ($Incident.Origin | AsDynamoMessageValue)) + $item.Add("TTL", ($unixTtl | AsDynamoMessageValue)) + + Write-Verbose "$logLead : Building Dynamo Put Object" + # Create new record in StatusfactionIncidents. + $putObject = New-Object -TypeName Amazon.DynamoDBv2.Model.Put + $putObject.TableName = "StatusfactionIncident" # This shouldn't change + $putObject.Item = $item + + $transaction = New-Object -TypeName Amazon.DynamoDBv2.Model.TransactWriteItem + $transaction.Put = $putObject + + Write-Verbose "$logLead : Send Dynamo Item Create" + try { + $params = @{ + TransactItem = $transaction + } + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $params["ProfileName"] = $ProfileName + } + + if (!([string]::IsNullOrEmpty($Region))) { + $params["Region"] = $Region + } + $dynamoResults = Write-DDBItemTransactionally @params + + Write-Verbose "$loglead : $($dynamoResults.HttpStatusCode)" + } + catch { + Write-Warning "$loglead : $_" + Write-Warning "$loglead : Failed to Create incident in Dynamo. SNS WAS sent, StatusPageIo incident WAS Created." + } + + } elseif ($Subject -eq "UpdateIncident"){ + # Update StatusfactionIncidents with new status + + if([string]::IsNullOrWhiteSpace($StatusfactionId)){ + Write-Error "$loglead : StatusfactionId is required for incident updates. Cannot continue." + } + + # Setup Update Object + $updateObject = New-Object -TypeName Amazon.DynamoDBv2.Model.Update + $updateObject.TableName = "StatusfactionIncident" # This shouldn't change + $updateObject.Key = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,Amazon.DynamoDBv2.Model.AttributeValue]' + $updateObject.Key.Add("Environment", ($Incident.ComponentName | AsDynamoMessageValue)) + $updateObject.Key.Add("Id", ($statusfactionId | AsDynamoMessageValue)) + + # Setup Update Expression + $updateObject.UpdateExpression = "SET #IncidentStatus = :incidentStatus" + + # Names are strings. Values are [AttributeValue]s + $updateObject.ExpressionAttributeNames = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string]' + $updateObject.ExpressionAttributeValues = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,Amazon.DynamoDBv2.Model.AttributeValue]' + + # Add Status + $updateObject.ExpressionAttributeNames.Add("#IncidentStatus", "IncidentStatus") + $updateObject.ExpressionAttributeValues.Add(":incidentStatus", ($Incident.IncidentStatus | AsDynamoMessageValue)) + + $transaction = New-Object -TypeName Amazon.DynamoDBv2.Model.TransactWriteItem + $transaction.Update = $updateObject + + try { + $params = @{ + TransactItem = $transaction + } + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $params["ProfileName"] = $ProfileName + } + + if (!([string]::IsNullOrEmpty($Region))) { + $params["Region"] = $Region + } + $dynamoResults = Write-DDBItemTransactionally @params + + Write-Verbose "$loglead : Dynamo Result - $($dynamoResults.HttpStatusCode)" + } + catch { + Write-Warning "$loglead : $_" + Write-Warning "$loglead : Failed to write updated incident to Dynamo. SNS WAS sent, StatusPageIo incident WAS updated." + } + } elseif ($Subject -eq "CloseAllIncidents"){ + # Since we're trying to close all open incidents, we'll not do anything with Dynamo here. + # The SNS message will find and close all open incidents by environment type. + Write-Verbose "$logLead : Subject $Subject" + } else { + Write-Error "$logLead : Unexpected Subject ( $subject ) type provided. Cannot continue." + } + + #EndRegion Dynamo + + #Region SNS + # Build Message Attributes + $messageAttributes = [System.Collections.HashTable]::new(@{ + Name = $Incident.IncidentName | AsSNSAttribute + Status = $Incident.IncidentStatus | AsSNSAttribute + PageId = $Incident.PageId | AsSNSAttribute + Component = $Incident.ComponentName | AsSNSAttribute + ComponentId = $Incident.ComponentId | AsSNSAttribute + ComponentStatus = $Incident.ComponentStatus | AsSNSAttribute + Origin = $Incident.Origin | AsSNSAttribute + }) + + # Always add our Id. + $messageAttributes.Add("StatusfactionId", ($statusfactionId | AsSNSAttribute)) + + # Send SNS Message + try { + $params = @{ + Message = $Message + MessageAttribute = $MessageAttributes + Subject = $Subject + TopicArn = $TopicArn + } + + if (!([string]::IsNullOrEmpty($ProfileName))) { + $params["ProfileName"] = $ProfileName + } + + if (!([string]::IsNullOrEmpty($Region))) { + $params["Region"] = $Region + } + + # This is fire and forget. You either get back a message ID, or it fails. We throw if it fails. + Write-Verbose "$loglead : Publishing SNS Message" + $response = Publish-SNSMessage @params + return $response + + # Throw if it fails. + } catch { + Write-Error "$logLead : Exception occurred when publishing SNS message: $($_.exception)" + } + #EndRegion SNS +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Metric.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Metric.Tests.ps1 new file mode 100644 index 0000000..403c742 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Metric.Tests.ps1 @@ -0,0 +1,56 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Send-Metric" { + Mock -CommandName New-TcpSocket -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName New-TcpStreamWriter -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Write-TcpSocketStreamWriter -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Push-TcpStreamWriterBuffer -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Close-TcpSocket -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Close-TcpStreamWriter -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Format-IpAddress -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Get-IPAddressesForName -MockWith { return $null } -ModuleName $moduleForMock + Mock -commandName Write-Warning -MockWith { return $null } + Mock -commandName Write-Verbose -MockWith { return $null } + Mock -commandName Write-Host -MockWith { return $null } + + Context "Input Validation" { + + It "Metric Param Cannot be empty" { + { Send-Metric -metric '' } | Should -Throw + } + It "Ip address override should override default url" { + Send-Metric -metric 'MockCounter:1|c' -Ip "127.0.0.1" + Assert-MockCalled Format-IpAddress -ParameterFilter { $Ip -eq "127.0.0.1" } -ModuleName $moduleForMock + } + It "defult URL should be overriden with supplied url" { + Send-Metric -metric 'MockCounter:1|c' -StatsdUrl "mockedURL.mock.domain.com" + Assert-MockCalled Get-IPAddressesForName -ParameterFilter { $hostname -eq "mockedURL.mock.domain.com" } -ModuleName $moduleForMock + } + It "defult URL should be used if no param supplied" { + Send-Metric -metric 'MockCounter:1|c' + Assert-MockCalled Get-IPAddressesForName -ParameterFilter { $hostname -eq "agg.os-logstash.haystack.prod.alkami.net" } -ModuleName $moduleForMock + } + It "defult IP should be used if empty string supplied" { + Send-Metric -metric 'MockCounter:1|c' -Ip "" + Assert-MockCalled Get-IPAddressesForName -ParameterFilter { $hostname -eq "agg.os-logstash.haystack.prod.alkami.net" } -ModuleName $moduleForMock + } + It "defult IP should be overidden if ip supplied" { + Send-Metric -metric 'MockCounter:1|c' -Ip "666.666.666.666" + Assert-MockCalled Format-IpAddress -ParameterFilter { $Ip -eq "666.666.666.666" } -ModuleName $moduleForMock + } + It "defult Port should be used if empty string supplied" { + Send-Metric -metric 'MockCounter:1|c' -Port "" + Assert-MockCalled New-TcpSocket -ParameterFilter { $Port -eq "2233" } -ModuleName $moduleForMock + } + It "defult port should be overidden if ip supplied" { + Send-Metric -metric 'MockCounter:1|c' -Port "66666" + Assert-MockCalled New-TcpSocket -ParameterFilter { $Port -eq "66666" } -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Metric.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Metric.ps1 new file mode 100644 index 0000000..9929aab --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Metric.ps1 @@ -0,0 +1,61 @@ +function Send-Metric { + <# +.SYNOPSIS + sends a metric to StatsD service over TCP. +.EXAMPLE + Send-Metric -Metric "bork.test:1|c" -verbose +.INPUTS + Metric: string of metric data to send + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default Port +#> + + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $Metric, + $StatsdUrl = "agg.os-logstash.haystack.prod.alkami.net", + $Ip, + $Port = 2233 + ) + $logLead = Get-LogLeadName + + if ([system.string]::IsNullOrEmpty($Ip)) { + Write-Verbose "$loglead : Using default ip." + $ipAddressString = @(Get-IPAddressesForName -hostname $StatsdUrl)[0] + } else { + Write-Warning "$loglead : Ip Address overridden with $Ip" + $ipAddressString = $Ip + } + + try { + Write-Verbose "$logLead : parsing ip $ipAddressString" + $ipAddress = Format-IpAddress -Ip $ipAddressString + } catch { + Write-Error "Failed to parse IP: $ipAddressString" + } + + try { + Write-Verbose "$logLead : Connecting TCP Socket" + $socket = New-TcpSocket -IpAddress $IpAddress -Port $port + Write-Verbose "$logLead : Opening Stream" + $writer = New-TcpStreamWriter -Socket $socket + } catch { + Write-Error "Failed to open socket and create stream: IpAddress $ipAddress Port $Port" + Write-Error $_ + } + + try { + Write-Verbose "$logLead : Attempting Write" + Write-TcpSocketStreamWriter -Writer $writer -Value $metric + Write-Verbose "$logLead : Attempting flush" + Push-TcpStreamWriterBuffer -Writer $writer + } finally { + Write-Verbose "$logLead : Closing Stream" + Close-TcpStreamWriter -Writer $writer + Write-Verbose "$logLead : Closing Socket" + Close-TcpSocket -Socket $Socket + } + Write-Host "$logLead : Sent $metric to $ipAddressString" +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-NewIncidentSnsMessage.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-NewIncidentSnsMessage.ps1 new file mode 100644 index 0000000..1782772 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-NewIncidentSnsMessage.ps1 @@ -0,0 +1,58 @@ +Function Send-NewIncidentSnsMessage { + <# +.SYNOPSIS + Sends an SNS message to create a new StatusPage.Io Incident. + +.PARAMETER Incident + Incident to create. + +.PARAMETER Message + Message to package in the SNS Message. Not especially important to the automation, but useful for human reading and logging. + +.PARAMETER StatusfactionId + Part of the Dynamo PK. Guid. Will be created if not supplied. + +.PARAMETER TopicArn + Arn of the SNS topic to send the message to. + +.PARAMETER ProfileName + AWS Creds. + +.PARAMETER Region + Aws Region. +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Incident, + [Parameter(Mandatory = $false)] + [string]$Message = "Creating New Incident", + [Parameter(Mandatory = $false)] + [string]$StatusfactionId, + [Parameter(Mandatory = $true)] + [string]$TopicArn, + [Parameter(Mandatory = $false)] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$Region + ) + $logLead = (Get-LogLeadName) + # Build SNS Message + + # Required value to correctly parse this on the lambda side. + $subject = "CreateIncident" + + $params = @{ + Message = $Message + Incident = $Incident + Subject = $subject + StatusfactionId = $StatusfactionId + TopicArn = $TopicArn + ProfileName = $ProfileName + Region = $Region + } + + Write-Verbose "$logLead : Sending Sns message." + return Send-IncidentSnsMessage @params +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-ResolveAllIncidentsSnsMessage.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-ResolveAllIncidentsSnsMessage.ps1 new file mode 100644 index 0000000..8e51a65 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-ResolveAllIncidentsSnsMessage.ps1 @@ -0,0 +1,51 @@ +Function Send-ResolveAllIncidentsSnsMessage { + <# + .SYNOPSIS + Sends an SNS message to resolve all existing StatusPage.Io Incidents for an environment. + + .PARAMETER Incident + Incident to modify. + + .PARAMETER Message + Message to package in the SNS Message. Not especially important to the automation, but useful for human reading and logging. + + .PARAMETER TopicArn + Arn of the SNS topic to send the message to. + + .PARAMETER ProfileName + AWS Creds. + + .PARAMETER Region + Aws Region. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Incident, + [Parameter(Mandatory = $false)] + [string]$Message = "Resolving All Open Incidents", + [Parameter(Mandatory = $true)] + [string]$TopicArn, + [Parameter(Mandatory = $false)] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$Region + ) + # Build SNS Message + + # Required value to correctly parse this on the lambda side. + $subject = "CloseAllIncidents" + + $params = @{ + Message = $Message + Incident = $Incident + StatusFactionId = [GUID]::Empty + Subject = $subject + TopicArn = $TopicArn + ProfileName = $ProfileName + Region = $Region + } + + return Send-IncidentSnsMessage @params +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Timer.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Timer.Tests.ps1 new file mode 100644 index 0000000..e4f0b8b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Timer.Tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Send-Timer" { + Mock -CommandName Send-Metric -MockWith { return $null } -ModuleName $moduleForMock + Mock -commandName Write-Verbose -MockWith { return $null } + + Context "Input Validation" { + + It "MetricName Cannot be empty" { + + { Send-Timer -MetricName '' } | Should -Throw + } + It "Time Cannot be empty" { + + { Send-Timer -Time ' ' } | Should -Throw + } + + } + Context "Param Passing is honored" { + + It "Supplied Ip is passed to Send-Metric" { + Send-Timer -MetricName "bork" -time 8 -Ip "666.666.666.666" + Assert-MockCalled Send-Metric -ParameterFilter { $Ip -eq "666.666.666.666" } -ModuleName $moduleForMock + } + It "Supplied Port is passed to Send-Metric" { + Send-Timer -MetricName "bork" -time 8 -Port 666 -verbose + Assert-MockCalled Send-Metric -ParameterFilter { $Port -eq "666" } -ModuleName $moduleForMock + } + It "Supplied StatsdUrl is passed to Send-Metric" { + Send-Timer -MetricName "bork" -time 8 -StatsdUrl "fake.fake.fake" + Assert-MockCalled Send-Metric -ParameterFilter { $StatsdUrl -eq "fake.fake.fake" } -ModuleName $moduleForMock + } + } + Context "Metric String Concat" { + + It "Metric is concatenated correctly and sent to Send-Metric" { + Send-Timer -MetricName "bork" -Time 8 + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:8|ms" } -ModuleName $moduleForMock + } + } + Context "Metric Counter" { + + It "accurate increment value is sent to Send-Counter" { + Send-Timer -MetricName "bork" -Time 1 -Ip "666.666.666.666" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|ms" } -ModuleName $moduleForMock + } + It "accurate increment string value is sent to Send-Counter" { + Send-Timer -MetricName "bork" -Time "1" -Ip "666.666.666.666" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|ms" } -ModuleName $moduleForMock + } + It "accurate decrement value is sent to Send-Counter" { + Send-Timer -MetricName "bork" -Time -1 -Port "666" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|ms" } -ModuleName $moduleForMock + } + It "accurate decrement string value is sent to Send-Counter" { + Send-Timer -MetricName "bork" -Time "-1" -Port "666" + Assert-MockCalled Send-Metric -ParameterFilter { $Metric -eq "bork:1|ms" } -ModuleName $moduleForMock + } + It "suppling string as count throws " { + { Send-Timer -MetricName "bork" -Time "fake" } | Should -Throw + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-Timer.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-Timer.ps1 new file mode 100644 index 0000000..24f35b3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-Timer.ps1 @@ -0,0 +1,46 @@ +function Send-Timer { + <# +.SYNOPSIS + Sends a Timer Metric to StatsD service. Time is in milliseconds. +.EXAMPLE + Send-timer -MetricName "bork" -time 8 -verbose +.INPUTS + Metric: namespace of metric + Time: value in Milliseconds + StatsdUrl: overrides default url + Ip: overrides dns resolution of url + Port : overrides default ip in Send-Metric +#> + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + $MetricName, + [ValidateNotNullOrEmpty()] + [int]$Time, + $StatsdUrl, + $Ip, + $Port + ) + $logLead = Get-LogLeadName + + [string]$metric = $MetricName + ":" + "$time" + "|ms" + + Write-Verbose "$logLead : Metric:$metric" + + $params = @{ } + if (![system.string]::IsNullOrEmpty($StatsdUrl)) { + Write-Verbose "StatsdUrl:$StatsdUrl" + $params += @{'statsdUrl' = $StatsdUrl } + } + if (![system.string]::IsNullOrEmpty($Port)) { + Write-Verbose "Port:$Port" + $params += @{'Port' = $Port } + } + if (![system.string]::IsNullOrEmpty($Ip)) { + Write-Verbose "Ip:$Ip" + $params += @{'Ip' = $Ip } + } + + Send-Metric -Metric $metric @params -Verbose:$VerbosePreference + +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Send-UpdateIncidentSnsMessage.ps1 b/Modules/Alkami.DevOps.Operations/Public/Send-UpdateIncidentSnsMessage.ps1 new file mode 100644 index 0000000..475526b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Send-UpdateIncidentSnsMessage.ps1 @@ -0,0 +1,56 @@ +Function Send-UpdateIncidentSnsMessage { + <# + .SYNOPSIS + Sends an SNS message to modify an existing StatusPage.Io Incident. + + .PARAMETER Incident + Incident to modify. + + .PARAMETER Message + Message to package in the SNS Message. Not especially important to the automation, but useful for human reading and logging. + + .PARAMETER StatusfactionId + Part of the Dynamo PK. Guid + + .PARAMETER TopicArn + Arn of the SNS topic to send the message to. + + .PARAMETER ProfileName + AWS Creds. + + .PARAMETER Region + Aws Region. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Incident, + [Parameter(Mandatory = $false)] + [string]$Message = "Updating Incident", + [Parameter(Mandatory = $true)] + [string]$StatusfactionId, + [Parameter(Mandatory = $true)] + [string]$TopicArn, + [Parameter(Mandatory = $false)] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$Region + ) + # Build SNS Message + + # Required value to correctly parse this on the lambda side. + $subject = "UpdateIncident" + + $params = @{ + Message = $Message + Incident = $Incident + StatusFactionId = $StatusfactionId + Subject = $subject + TopicArn = $TopicArn + ProfileName = $ProfileName + Region = $Region + } + + return Send-IncidentSnsMessage @params +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.ps1 new file mode 100644 index 0000000..5136e44 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.ps1 @@ -0,0 +1,300 @@ +function Set-ASInstanceState { +<# +.SYNOPSIS + Sets an EC2 Instance to Active or Standby on the ASG. In Prod/Staging, this function can only be run against an App tier or Entrust tier AWS server. In Dev/QA Webs are acceptable as well. + +.DESCRIPTION + Sets an EC2 Instance to Active or Standby in the ASG after first setting the instance health on the ASG to Healthy if required. Setting the instance to standby removes it from the NLB and setting it to active reinstates it. + May run against a single remote machine. If no machine name parameter is specified, executes against localhost. + In Prod/Staging this function can only be run against an App tier or Entrust tier AWS server. In Dev/QA Web tier servers are also valid targets. + Will only set the instance to Standby if the other remaining instances in the ASG are healthy, unless -Force is used. + +.PARAMETER ComputerName + Optional FQDN of the machine to set to active or standby + +.PARAMETER InstanceId + Optional instance id of the machine to set to active or standby + +.PARAMETER ProfileName + Optional profile name. Preferred that it be provided. + +.PARAMETER Region + Optional region name. Preferred that it be provided. + +.PARAMETER Timeout + The length of time in minutes to wait for a server to respond to Get-ASInstanceState. Defaults to 20 (minutes) + +.PARAMETER Force + Optional boolean to force setting the instance to Standby regardless of the health state of other instances in the ASG. + +.EXAMPLE + Set-ASInstanceState -standby + + [Set-ASInstanceState] : Attempting to Set Computer 127.0.0.1 to Standby + [Set-ASInstanceState] : Setting Instance i-0e5760981561dac91 to Standby + [Set-ASInstanceState] : Standby Request Returned a Status of InProgress. Moving EC2 instance to Standby: i-0e5760981561dac91 + +.EXAMPLE + Set-ASInstanceState -computerName foobar.fh.local -standby + + [Set-ASInstanceState] : Creating Session to Remote Computer foobar.fh.local + [Set-ASInstanceState] : Attempting to Set Computer foobar.fh.local to Standby + [Set-ASInstanceState] : Setting Instance i-0fd7b8a51b4d39906 to Standby + [Set-ASInstanceState] : Standby Request Returned a Status of InProgress. Moving EC2 instance to Standby: i-0fd7b8a51b4d39906 + +.EXAMPLE + Set-ASInstanceState -computerName foobar.fh.local -standby + + [Set-ASInstanceState] : Creating Session to Remote Computer foobar.fh.local + [Set-ASInstanceState] : Attempting to Set Computer foobar.fh.local to Standby + WARNING: [Set-ASInstanceState] : Stopping because at least one remaining instance in the ASG 0.2_Sandbox_app-20190612164333406500000006 must be healthy before changing instance status to Standby. Use -Force to ignore warning and continue. + WARNING: [Set-ASInstanceState] : Null response received from script block. Review the output for errors. + +.EXAMPLE + Set-ASInstanceState -ComputerName foobar.fh.local -Standby -Force + + [Set-ASInstanceState] : Creating Session to Remote Computer foobar.fh.local + [Set-ASInstanceState] : Attempting to Set Computer foobar.fh.local to Standby + WARNING: [Set-ASInstanceState] : At least one remaining instance in the ASG 0.2_Sandbox_app-20190612164333406500000006 must be healthy before changing instance status to Standby. -Force was used so continuing. + [Set-ASInstanceState] : Setting Instance i-0e5760981561dac91 to Standby + [Set-ASInstanceState] : Standby Request Returned a Status of InProgress. Moving EC2 instance to Standby: i-0fd7b8a51b4d39906 + +.EXAMPLE + Set-ASInstanceState -active + + [Set-ASInstanceState] : Attempting to Set Computer 127.0.0.1 to Active + [Set-ASInstanceState] : Setting Instance i-0e5760981561dac91 to Active + [Set-ASInstanceState] : Active Request Returned a Status of PreInService. Moving EC2 instance out of Standby: i-0e5760981561dac91 + +.EXAMPLE + Set-ASInstanceState -computerName foobar.fh.local -active + + [Set-ASInstanceState] : Creating Session to Remote Computer foobar.fh.local + [Set-ASInstanceState] : Attempting to Set Computer foobar.fh.local to Active + [Set-ASInstanceState] : Updating Instance i-0e5760981561dac91 Status from UNHEALTHY to Healthy + [Set-ASInstanceState] : Setting Instance i-0e5760981561dac91 to Active + [Set-ASInstanceState] : Active Request Returned a Status of PreInService. Moving EC2 instance out of Standby: i-0e5760981561dac91 +#> + + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param( + + [Parameter(ParameterSetName = 'StandbyParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = "ComputerName", Mandatory = $false)] + [Parameter(ParameterSetName = "InstanceId", Mandatory = $false)] + [Alias("Offline")] + [switch]$Standby, + + [Parameter(ParameterSetName = 'ActiveParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = "ComputerName", Mandatory = $false)] + [Parameter(ParameterSetName = "InstanceId", Mandatory = $false)] + [Alias("Online")] + [switch]$Active, + + [Parameter(ParameterSetName = 'ActiveParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'StandbyParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ComputerName', Mandatory = $false)] + [Alias("MachineName", "ServerName")] + [string]$ComputerName, + + [Parameter(ParameterSetName = 'ActiveParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'StandbyParameterSet', Mandatory = $true)] + [Parameter(ParameterSetName = "InstanceId", Mandatory = $true)] + [string]$InstanceId, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region, + + [Parameter(Mandatory = $false)] + [int]$Timeout = 20, + + [Parameter(Mandatory = $false)] + [Alias("f")] + [switch]$Force + ) + + $logLead = Get-LogLeadName + + Import-AWSModule # AS + + if ($Standby) { + $stateText = "Standby" + } elseif($Active) { + $stateText = "Active" + } else { + Write-Warning "Either the -Active or -Standby switch is required." + throw New-Object System.ArgumentException "Either the -Active or -Standby switch is required." + } + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + # Not sure if this is better or just $env:ComputerName + $ComputerName = Get-FullyQualifiedServerName + } + + # TODO: TechDebt ticket to fix this? + $IsValid = ( + (Test-IsWebServer -ComputerName $ComputerName) -or + (Test-IsAppServer -ComputerName $ComputerName) -or + (Test-IsEntrustServer) + ) + $IsAwsValidTarget = (Test-IsAws) -or $IsValid + if (!$IsAwsValidTarget) { + Write-Warning "$logLead : This function can only be run on an AWS App or Entrust server. Was targeted for [$computerName]" + return + } + + $instanceRegion = $Region + + if ([string]::IsNullOrWhiteSpace($instanceRegion)) { + $instanceRegion = Get-AwsRegionByHostname -ComputerName $ComputerName + } + + if ([string]::IsNullOrWhiteSpace($InstanceId)) { + $InstanceId = @(Get-EC2InstancesByHostname -Servers $ComputerName -ProfileName $ProfileName)[0].InstanceId + } + + Write-Host "$logLead : Getting Current AutoScalingInstance." + $autoScalingInstance = Get-ASAutoScalingInstance -InstanceID $InstanceId -Region $instanceRegion -ProfileName $ProfileName + + if ($null -eq $autoScalingInstance) { + Write-Warning "$logLead : Could not retrieve the parent ASG Instance with ID $InstanceId." + return + } + + $healthStatus = $autoScalingInstance.HealthStatus + $asgName = $autoScalingInstance.AutoScalingGroupName + + Write-Host "$logLead : ASG $asgName Reports Instance is $healthStatus" + + $desiredHealthStatus = "Healthy" + if ($healthStatus -ine $desiredHealthStatus) { + + Write-Host "$logLead : Updating Instance $InstanceId Status from $healthStatus to $desiredHealthStatus" + Set-ASInstanceHealth -InstanceId $InstanceId -HealthStatus $desiredHealthStatus -Force -Region $instanceRegion -ProfileName $ProfileName -ShouldRespectGracePeriod $false -ErrorAction Stop + + Write-Verbose "$logLead : Sleeping for 5 Seconds" + Start-Sleep -Seconds 5 + } + + # When setting to Standby, Check the health of all of the other instances in the ASG and fail if the only remaining instances are Unhealthy so outages don't occur. + if ($Standby) { + $autoscalingGroupTagKey = "aws:autoscaling:groupName" + + $currentInstanceTags = (Get-EC2Instance -InstanceId $InstanceId -Region $instanceRegion -ProfileName $ProfileName).Instances[0].Tags + $autoScalingGroupName = $currentInstanceTags | Where-Object { $_.Key -eq $autoscalingGroupTagKey } | Select-Object -First 1 -ExpandProperty Value + $groupMembers = (Get-InstancesByTag -tags @{ $autoscalingGroupTagKey = $autoScalingGroupName } -Region $instanceRegion -ProfileName $ProfileName).InstanceId + + Write-Host "$loglead : Found the following instances on ASG $asgName :" + foreach ($groupMember in $groupMembers) { + Write-Host "$loglead : $groupMember" + } + + $remainingInstances = (Get-ASAutoScalingInstance -InstanceID $groupMembers -Region $instanceRegion -ProfileName $ProfileName) | Where-Object { ($_.InstanceId -ine $InstanceId) -and ($_.HealthStatus -eq $desiredHealthStatus) } + + if ($remainingInstances.Count -eq 0) { + + if ($Force) { + Write-Warning "$loglead : At least one remaining instance in the ASG $asgName must be healthy before changing instance status to Standby. -Force was used so continuing." + } else { + Write-Error "$loglead : Stopping - at least one remaining instance in the ASG $asgName must be healthy before changing instance status to Standby. Use -Force to ignore warning and continue." + return + } + } else { + Write-Host "$logLead : At least one remaining instance in the ASG $asgName is healthy." + } + } + + Write-Host "$logLead : Setting Instance $InstanceId to $stateText" + + $stopWatch = [System.Diagnostics.StopWatch]::StartNew() + + ## attempt to remediate if the state isn't what we expect before we go into setting the final state + $existingState = Get-ASInstanceState -InstanceId $InstanceId -ProfileName $ProfileName + $wasPending = $existingState -match 'Pending' + if ($wasPending) { + Write-Warning "$logLead : Current instance state is [$existingState], waiting to see if it changes to non-pending on a 10s looping interval" + } + while ($existingState -match 'Pending') { + Start-Sleep -Seconds 10 + + if ($stopWatch.Elapsed.TotalMinutes -gt $Timeout) { + Write-Warning "$logLead : The server check for Get-ASInstanceState for $computerName took longer than $Timeout minutes to complete." + throw "$logLead : Took longer than $Timeout minutes to remove server $server from the load balancer. Investigate." + } + + $existingState = Get-ASInstanceState -InstanceId $InstanceId -ProfileName $ProfileName + } + $wasPendingSeconds = 0 + if ($wasPending) { + Write-Warning "$logLead : Left pending state, current state now [$existingState]" + $wasPendingSeconds = $stopWatch.Elapsed.TotalSeconds + } + + ## We ensured we weren't in the Pending states, so we can only be in Active, Standby, or idk-state + if (@('Active', 'Standby') -notContains $existingState) { + $errorMessage = "Please investigate the health of the ASG [$asgName] instance [$InstanceId] in the [$instanceRegion] Region" + Write-Warning "$logLead : Get-ASInstanceState returns [$existingState]" + Write-Warning "$logLead : Get-ASInstanceState expected results were either Active or Standby" + Write-Warning "$logLead : $errorMessage" + Write-Warning "$logLead : Failing case, can not continue. Aborting via throw" + throw $errorMessage + } + + if ($existingState -eq $stateText) { + Write-Warning "$logLead : Was asked to put instance into current state [$existingState]. Nothing to do, state change not required." + return @{ StatusMessage = "Was asked to put instance into current state [$existingState]. Nothing to do, state change not required."; Description = "No work to do"; StatusCode = "nothing to do (Alkami)" } + } + + ## At this point, we only have two remaining states + ## Standby -> Should be Active + ## Active -> Should be Standby + $readyForActive = ($Standby -and ($existingState -eq 'Active')) + $readyForStandby = (!$Standby -and ($existingState -eq 'Standby')) + + if (!$readyForActive -and !$readyForStandby) { + ## In order to be _not_ ready for active, and also _not_ ready for standby, something has to be wrong + ## We should have already gotten to that state above + ## We handled "current state is still pending for some reason" + ## We handled "current state wasn't one of the two expected states" + ## We handled "current state is what was asked for" + ## So at this point, if we trip this metric, I don't even know what to fail on. Just dump a lot of debug info and let's see what we can discern. + Write-Warning "$logLead : Please investigate the health of the ASG [$asgName] instance [$InstanceId] in the [$instanceRegion] Region" + Write-Warning "$logLead : Requested state was [$stateText]. Existing state determined to be [$existingState]. SetStandby: $Standby" + Write-Warning "$logLead : Health is '$(Get-ASInstanceHealth)'" + Write-Warning "$logLead : -" + Write-Warning "$logLead : Logic problem determined in code, can not determine how to proceed. Please open a ticket with SRE-RelEng with this information and what you found during investigation." + } + + $actionResult = $null + + if ($Standby) { + $actionResult = Enter-ASStandby -AutoScalingGroupName $asgName -InstanceId $InstanceId -ShouldDecrementDesiredCapacity $true -Region $instanceRegion -ProfileName $ProfileName -ErrorAction "Continue" + } else { + $actionResult = Exit-ASStandby -AutoScalingGroupName $asgName -InstanceId $InstanceId -Region $instanceRegion -ProfileName $ProfileName -ErrorAction "Continue" + } + + ## Get-ASInstanceState will grab the local server if none are specified + while ($stateText -ne (Get-ASInstanceState -InstanceId $InstanceId -ProfileName $ProfileName)) { + Start-Sleep -Seconds 10 + + if($stopWatch.Elapsed.TotalMinutes -gt $Timeout) { + Write-Warning "$logLead : The server check for Get-ASInstanceState for $computerName took longer than $Timeout minutes to complete." + if ($wasPending) { + Write-Warning "$logLead : Part of the above timeout ($wasPendingSeconds seconds) was from waiting on the pending status change" + } + throw "$logLead : Took longer than $Timeout minutes to remove server $server from the load balancer. Investigate." + } + } + + if ($null -ne $actionResult) { + $coalesceResult = Test-IsNull $actionResult.StatusMessage $actionResult.Description -Strict + Write-Warning ("$logLead : $stateText Request Returned a Status of $($actionResult.StatusCode). {0}" -f $coalesceResult) + } + else { + Write-Warning "$logLead : Null response received from script block. Review the output for errors." + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.tests.ps1 new file mode 100644 index 0000000..76fab29 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-ASInstanceState.tests.ps1 @@ -0,0 +1,64 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-ASInstanceState" { + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + #Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + + Mock -CommandName Get-LogLeadName -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Import-AWSModule -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-FullyQualifiedServerName -MockWith { return "my.fakeserver.name"} -ModuleName $moduleForMock + Mock -CommandName Test-IsWebServer -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Test-IsAppServer -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Test-IsEntrustServer -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Test-IsAws -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-AwsRegionByHostname -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-EC2InstancesByHostname -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ASAutoScalingInstance -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ASInstanceHealth -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Start-Sleep -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-EC2Instance -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-InstancesByTag -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ASInstanceState -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Enter-ASStandby -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Test-IsNull -MockWith {} -ModuleName $moduleForMock + + Context "When Neither Standby Nor Active Switch Is Provided" { + It "Throws" { + { Set-ASInstanceState } | Should -Throw + } + } + + Context "When ComputerName is null" { + It "Gets the servername" { + Set-ASInstanceState -Active + Assert-MockCalled Get-FullyQualifiedServerName -ModuleName $moduleForMock + } + } + + Context "When ComputerName is empty" { + It "Gets the servername" { + Set-ASInstanceState -Active -ComputerName " " + Assert-MockCalled Get-FullyQualifiedServerName -ModuleName $moduleForMock + } + } + + Context "When Is Not A Valid AWS Target"{ + Mock -CommandName Test-IsWebServer -MockWith {return $false} -ModuleName $moduleForMock + Mock -CommandName Test-IsAppServer -MockWith {return $false} -ModuleName $moduleForMock + Mock -CommandName Test-IsEntrustServer -MockWith {return $false} -ModuleName $moduleForMock + + It "Writes a Warning"{ + Set-ASInstanceState -Active -ComputerName " " + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -like "*This function can only be run on an AWS App or Entrust server. Was targeted for*" } + + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.Tests.ps1 new file mode 100644 index 0000000..e969b2c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.Tests.ps1 @@ -0,0 +1,77 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-EC2InstanceTagObject" { + + Mock -CommandName Get-EC2InstancesByHostname -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-CurrentInstanceId -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + Mock -CommandName New-EC2Tag -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Import-AWSModule -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Error -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-SupportedAwsRegions -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Test-IsAws -MockWith {} -ModuleName $moduleForMock + Mock -CommandName New-Object -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-LogLeadName -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Compare-StringToLocalMachineIdentifiers -MockWith {return $false} -ModuleName $moduleForMock + + It "Not AWS and ProfileName is empty it throws" { + Mock -CommandName Test-IsAws -MockWith {return $false} -ModuleName $moduleForMock + $InstanceId = "FakeInstanceId" + $Key = "Test" + $Value = "TestValue" + $ProfileName = "" + $Region = "FakeRegion" + { Set-EC2InstanceTag -InstanceId $InstanceId -Key $Key -Value $Value -ProfileName $ProfileName -Region $Region } | Should -Throw + } + + It "Region isn't in supported AWS regions it throws" { + Mock -CommandName Test-IsAws -MockWith {return $true} -ModuleName $moduleForMock + Mock -CommandName Get-SupportedAwsRegions -MockWith {return "TestRegion"} -ModuleName $moduleForMock + $InstanceId = "FakeInstanceId" + $Key = "Test" + $Value = "TestValue" + $ProfileName = "" + $Region = "FakeRegion" + { Set-EC2InstanceTag -InstanceId $InstanceId -Key $Key -Value $Value -ProfileName $ProfileName -Region $Region } | Should -Throw + } + + It "ComputerName is populated and it equals localhost " { + Mock -CommandName Test-IsAws -MockWith {return $true} -ModuleName $moduleForMock + Mock -CommandName Get-SupportedAwsRegions -MockWith {return "TestRegion"} -ModuleName $moduleForMock + Mock -CommandName Get-CurrentInstanceId -MockWith {return "TestId"} -ModuleName $moduleForMock + Mock -CommandName Compare-StringToLocalMachineIdentifiers -MockWith {return $true} -ModuleName $moduleForMock + + $ComputerName = "localhost" + $Key = "Test" + $Value = "TestValue" + $ProfileName = "TestProfileName" + $Region = "TestRegion" + Set-EC2InstanceTag -ComputerName $ComputerName -Key $Key -Value $Value -ProfileName $ProfileName -Region $Region + Assert-MockCalled Get-CurrentInstanceId -ModuleName $moduleForMock + } + + It "ComputerName is populated and it doesn't equal localhost " { + Mock -CommandName Test-IsAws -MockWith {return $true} -ModuleName $moduleForMock + Mock -CommandName Get-SupportedAwsRegions -MockWith {return "TestRegion"} -ModuleName $moduleForMock + Mock -CommandName Get-EC2InstancesByHostname -MockWith { + return @{InstanceId = 'fakeInstanceId' } + } -ModuleName $moduleForMock + Mock -CommandName Compare-StringToLocalMachineIdentifiers -MockWith {return $false} -ModuleName $moduleForMock + + $ComputerName = "TestName" + $Key = "Test" + $Value = "TestValue" + $ProfileName = "TestProfileName" + $Region = "TestRegion" + Set-EC2InstanceTag -ComputerName $ComputerName -Key $Key -Value $Value -ProfileName $ProfileName -Region $Region + Assert-MockCalled Get-EC2InstancesByHostname -ModuleName $moduleForMock + } + + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.ps1 new file mode 100644 index 0000000..c0fe47e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-EC2InstanceTag.ps1 @@ -0,0 +1,99 @@ +function Set-EC2InstanceTag { +<# +.SYNOPSIS + Returns EC2 instance objects by computername or instance id. + +.PARAMETER ComputerName + The Computer Name to search for. + +.PARAMETER InstanceId + The Instance ID to search for. + +.PARAMETER ProfileName + The AWS profile name to use when querying for EC2 instances. ProfileName needed for 'ByInstanceId' if not on an AWS machine. Also note, if a ProfileName is not supplied, that instance's default ProfileName may not have the right permissions to add a tag or view instances. + +.PARAMETER Region + The Region to use when applying a tag to an EC2 instance. + +.PARAMETER Key + The key to assign to the new tag. + +.PARAMETER Value + The value to assign to the new tag. + +.EXAMPLE + + By InstanceId: Set-EC2InstanceTag -InstanceId "i-0d62fcd1a9ad41ff1" -Key "testing" -Value "wow" -Region "us-east-1" -ProfileName "temp-dev" + + By InstanceId on AWS Machine: Set-EC2InstanceTag -InstanceId "i-0d62fcd1a9ad41ff1" -Key "testing" -Value "wow" -Region "us-east-1" + + By ComputerName: Set-EC2InstanceTag -ComputerName "mic27788" -Key "testing2" -Value "wow2" -Region "us-east-1" -ProfileName "temp-dev" + +#> + + [CmdletBinding()] + param( + [Parameter(ParameterSetName = 'ByComputerName', Mandatory = $true)] + $ComputerName, + [Parameter(ParameterSetName = 'ByInstanceId', Mandatory = $true)] + $InstanceId, + [Parameter(ParameterSetName = 'ByComputerName', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByInstanceId', Mandatory = $false)] + $ProfileName, + [Parameter(ParameterSetName = 'ByComputerName', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByInstanceId', Mandatory = $true)] + $Region, + [Parameter(ParameterSetName = 'ByComputerName', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByInstanceId', Mandatory = $true)] + $Key, + [Parameter(ParameterSetName = 'ByComputerName', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByInstanceId', Mandatory = $true)] + $Value + ) + + $logLead = Get-LogLeadName + + if (!(Test-IsAws)) { + if ([string]::IsNullOrEmpty($ProfileName)) { + Write-Error "$logLead : ProfileName not supplied on non-AWS machine, please supply a ProfileName parameter" + throw + } + } + if ($Region -notin (Get-SupportedAwsRegions)) { + Write-Error "$logLead : Region not valid, please supply a valid Region" + throw + } + + + if (!([string]::IsNullOrEmpty($ComputerName))) { + if (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName) { + $InstanceId = Get-CurrentInstanceId + } else { + $tempInstanceObject = Get-EC2InstancesByHostname -Servers $ComputerName -ProfileName $ProfileName + $InstanceId = $tempInstanceObject.InstanceId + } + } + + if ([string]::IsNullOrEmpty($InstanceId)) { + Write-Error "$logLead : InstanceId not found" + throw + } + + Import-AWSModule + $tag = New-Object Amazon.EC2.Model.Tag -Property @{ Key = "$Key"; Value = "$Value" } + + try { + if ([string]::IsNullOrEmpty($ProfileName)) { + New-EC2Tag -Resource $InstanceId -Tag $tag -Region $Region + } else { + New-EC2Tag -Resource $InstanceId -Tag $tag -ProfileName $ProfileName -Region $Region + } + $success = $true + } catch { + $tempException = $_ + Write-Warning "Could not set New EC2 Tag on Instance ID [$InstanceId]" + Write-Warning $tempException.Exception.Message + $success = $false + } + return $success +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.ps1 new file mode 100644 index 0000000..9ae723b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.ps1 @@ -0,0 +1,126 @@ +function Set-LoadBalancerState { + <# + .SYNOPSIS + Handles taking the server in/out of the load balancer appropriate to the type of server. + Supports Web/App servers. Mic/Fab servers are no-ops. + + .PARAMETER Server + The string to get the server type string from. + + .PARAMETER DesiredState + The load balancer state to put the server in. Up or Down. + + .PARAMETER AwsProfileName + The AWS Profile the Server is in. + + .PARAMETER AwsRegion + The AWS Region the Server is in. + #> + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [string]$Server, + [Parameter(Mandatory = $true)] + [ValidateSet("down", "up")] + [string]$DesiredState, + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + [Parameter(Mandatory = $false)] + [string]$AwsRegion, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + $logLead = (Get-LogLeadName) + + $arguments = @{ + desiredState = $DesiredState + hostname = $Server + AwsProfileName = $AwsProfileName + AwsRegion = $AwsRegion + force = $Force + } + # Get the string type of the server. + $serverType = Get-ServerTypeByHostname -ComputerName $Server + + # Early out if it's a mic or fab server. These don't use load balancers. + if (($serverType -eq "Mic") -or ($serverType -eq "Fab")) { + Write-Host "$logLead : $serverType servers do not use a load balancer. Exiting." + return + } + + # Throw if this is an unsupported/unknown server type. If it's not a Web or App. + if (!(($serverType -eq "Web") -or ($serverType -eq "App"))) { + throw "$logLead : Setting a load balancer on server type $serverType for host $Server is unsupported." + } + + # Determine what type of load balancer the environment uses. + # Only Staging/Production web servers use nginx, and everything else is AWS load balancers. + $environmentType = Get-AppSetting -Key "Environment.Type" -ComputerName $Server + + # If we can't get the Environment type, then either the server is off, it doesn't exist, or it's misconfigured. + # In any case, we should remove it from the LB. + if ($null -eq $environmentType) { + Write-Warning "$logLead : Could not retrieve environment type. Removing $Server from any potential loadbalancers." + + # If this is a web server in Prod/Staging, it's handled by Nginx. We should attempt to remove it from the LB there first. + try { + $arguments["desiredState"] = "down" + Write-Host "$loglead : Attempting to set Nginx to Down" + $setNginxHostStateResult = Set-NginxHostState @arguments + } catch { + # If the Nginx call failed, then we try AWS load balancers. + try { + Write-Warning "$logLead : Changing nginx state of host $Server failed." + Write-Host "$logLead : Attempting to remove from AWS." + Set-ASInstanceState -Standby -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + } catch { + Write-Warning "$logLead : Could not remove $Server from either load balancer. Check that the server exists in a load balancer." + Write-Warning "$($_.Exception.Message)" + return "Fail" + } + } + } else { + $useNginx = $serverType -eq "Web" -and (($environmentType -eq "Staging") -or ($environmentType -eq "Production")) + $loadBalancerType = if ($useNginx) { "NGINX" } else { "AWS" } + Write-Host "$logLead : Determined that server uses an $loadBalancerType load balancer." + + # Write friendly state change message. + if ($arguments.desiredState -eq "down") { + Write-Host "$logLead : Removing server $Server from the load balancer." + } else { + Write-Host "$logLead : Adding server $Server to the load balancer." + } + + if ($useNginx) { + # Handle nginx load balancers. + $setNginxHostStateResult = Set-NginxHostState @arguments + if ($setNginxHostStateResult -eq "Fail") { + Write-Warning "$logLead : Changing nginx state of host $Server failed." + return "Fail" + } + } else { + try { + # Handle AWS load balancers. + # Do not pass down a profile name / region because the commands are run from the remote machine. + if ($arguments.desiredState -eq "down") { + Set-ASInstanceState -Standby -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + } else { + Set-ASInstanceState -Active -ComputerName $Server -ProfileName $AwsProfileName -Region $AwsRegion + } + } catch { + Write-Warning "$logLead : Changing aws LB state of host $Server failed." + Write-Warning "$($_.Exception.Message)" + return "Fail" + } + } + + # Write friendly completion message. + if ($arguments.desiredState -eq "down") { + Write-Host "$logLead : $Server successfully removed from the load balancer." + } else { + Write-Host "$logLead : $Server successfully added to the load balancer." + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.tests.ps1 new file mode 100644 index 0000000..aa26e2e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-LoadBalancerState.tests.ps1 @@ -0,0 +1,316 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-LoadBalancerState" { + Mock Get-LogLeadName -ModuleName $moduleForMock + Mock Write-Host -ModuleName $moduleForMock + Mock Write-Warning -ModuleName $moduleForMock + Mock Set-NginxHostState -ModuleName $moduleForMock + Mock Set-ASInstanceState -ModuleName $moduleForMock + Mock Write-Error -ModuleName $moduleForMock + + $paramList = @{ + desiredState = $DesiredState + hostname = $Server + AwsProfileName = $AwsProfileName + AwsRegion = $AwsRegion + force = $Force + } + Context "Early returns" { + It "returns early with wrong server type: mic" { + Set-LoadBalancerState -Server "mic666.notfh.remote" -DesiredState "up" | Should -Be $null + } + It "DesiredState down, returns early with wrong server type: mic" { + Set-LoadBalancerState -Server "mic666.notfh.remote" -DesiredState "down" | Should -Be $null + } + It "returns early with wrong server type: fab" { + Set-LoadBalancerState -Server "fab666.notfh.remote" -DesiredState "up" | Should -Be $null + } + It "DesiredState down, returns early with wrong server type: fab" { + Set-LoadBalancerState -Server "fab666.notfh.remote" -DesiredState "down" | Should -Be $null + } + It "not a valid server type" { + { Set-LoadBalancerState -Server "bork666.notfh.remote" -DesiredState "up" } | Should -Throw + } + It "DesiredState down, not a valid server type" { + { Set-LoadBalancerState -Server "bork666.notfh.remote" -DesiredState "down" } | Should -Throw + } + } + Context "null Env.type" { + $paramList = @{ + desiredState = "up" + hostname = "web666.notfh.remote" + AwsProfileName = $null + AwsRegion = $null + force = $false + } + Mock Get-AppSetting { return $null } -ModuleName $moduleForMock + + It "warns user that server is being pulled from LBs" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -eq " : Could not retrieve environment type. Removing $($paramList.hostname) from any potential loadbalancers." } + } + It "nginx called with down params, even if Set-LBS called with UP" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -ParameterFilter { $desiredState -eq "down" } + } + It "nginx called with down params, even if Set-LBS not called with UP" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $desiredState -eq "up" } + } + It "Set-ASIS called with standby params, even if Set-LBS called with UP" { + Mock Set-NginxHostState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $computername -eq $paramList.hostname } + } + It "Set-ASIS not called with active params, even if Set-LBS called with UP" { + Mock Set-NginxHostState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $Actve.IsPresent -eq $false -and $computername -eq $paramList.hostname } + } + It "IF Set-ASIS throws, we return 'Fail'" { + Mock Set-NginxHostState { throw } -ModuleName $moduleForMock + Mock Set-ASInstanceState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + } + Context "staging nginx code path" { + $paramList = @{ + desiredState = "up" + hostname = "web666.notfh.remote" + AwsProfileName = $null + AwsRegion = $null + force = $false + } + Mock Get-AppSetting { return "Staging" } -ModuleName $moduleForMock + + It "Set-NHS called with up params,if Set-LBS called with UP" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -ParameterFilter { $desiredState -eq "up" } + } + It "if Set-LBS called with UP, Set-NHS not called with down params" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $desiredState -eq "down" } + } + It "Set-NHS returns 'Fail' Set-LBS also returns 'Fail'" { + Mock Set-NginxHostState { return "Fail" } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + } + Context "Production nginx code path" { + $paramList = @{ + desiredState = "up" + hostname = "web666.notfh.remote" + AwsProfileName = $null + AwsRegion = $null + force = $false + } + Mock Get-AppSetting { return "Production" } -ModuleName $moduleForMock + + It "Set-NHS called with up params,if Set-LBS called with UP" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -ParameterFilter { $desiredState -eq "up" } + } + It "Set-NHS not called with down params, if Set-LBS called with UP" { + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-NginxHostState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $desiredState -eq "down" } + } + It "Set-NHS returns 'Fail' Set-LBS also returns 'Fail'" { + Mock Set-NginxHostState { return "Fail" } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + } + Context "staging aws lb code path" { + BeforeEach{ + $paramList = @{ + desiredState = $null + hostname = "app666.notfh.remote" + AwsProfileName = $null + AwsRegion = $null + force = $false + } + } + + Mock Get-AppSetting { return "Staging" } + + It "if Set-LBS called up, Set-ASIS called Active" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called up, Set-ASIS does not call standy" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $standby.IsPresent -eq $false -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called down, Set-ASIS called Standby" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called down, Set-ASIS does not call active" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $Active.IsPresent -eq $false -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws profile arg check" { + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called down, Set-ASIS called Standby, with aws profile arg check" { + $paramList.desiredState = "down" + $paramList.AwsProfileName = "expectedAWSProfileName" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region arg check" { + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion } + } + It "if Set-LBS called down, Set-ASIS called Standby, with aws region arg check" { + $paramList.desiredState = "down" + $paramList.AwsRegion = "expectedAWSRegion" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region and profile arg check" { + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region and profile arg check" { + $paramList.desiredState = "down" + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion -and $ProfileName -eq $paramList.AwsProfileName } + } + It "Set-ASIS throws, Set-LBS also returns 'Fail'" { + $paramList.desiredState = "up" + Mock Set-ASInstanceState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + It "Set-ASIS throws, Set-LBS also returns 'Fail'" { + $paramList.desiredState = "down" + Mock Set-ASInstanceState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + It "warns user that server is being pulled from LBs" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -eq " : Changing aws LB state of host $($paramList.hostname) failed." } + } + It "warns user that server is being pulled from LBs" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -eq " : Changing aws LB state of host $($paramList.hostname) failed." } + } + } + Context "Production aws lb code path" { + BeforeEach{ + $paramList = @{ + desiredState = $null + hostname = "app666.notfh.remote" + AwsProfileName = $null + AwsRegion = $null + force = $false + } + } + Mock Get-AppSetting { return "Production" } + + It "if Set-LBS called up, Set-ASIS called Active" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called up, Set-ASIS does not call standby" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $standby.IsPresent -eq $false -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called down, Set-ASIS called Standby" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called down, Set-ASIS does not call active" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -Times 0 -ParameterFilter { $Active.IsPresent -eq $false -and $ComputerName -eq $paramList.hostname } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws profile arg check" { + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called down, Set-ASIS called Standby, with aws profile arg check" { + $paramList.desiredState = "down" + $paramList.AwsProfileName = "expectedAWSProfileName" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region arg check" { + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion } + } + It "if Set-LBS called down, Set-ASIS called Standby, with aws region arg check" { + $paramList.desiredState = "down" + $paramList.AwsRegion = "expectedAWSRegion" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Standby.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region and profile arg check" { + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion -and $ProfileName -eq $paramList.AwsProfileName } + } + It "if Set-LBS called up, Set-ASIS called Active, with aws region and profile arg check" { + $paramList.desiredState = "down" + $paramList.AwsRegion = "expectedAWSRegion" + $paramList.AwsProfileName = "expectedAWSProfileName" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState -AwsRegion $paramList.AwsRegion -AwsProfileName $paramList.AwsProfileName + Assert-MockCalled Set-ASInstanceState -ModuleName $moduleForMock -ParameterFilter { $Active.IsPresent -eq $true -and $ComputerName -eq $paramList.hostname -and $Region -eq $paramList.AwsRegion -and $ProfileName -eq $paramList.AwsProfileName } + } + It "Set-ASIS throws, Set-LBS also returns 'Fail'" { + $paramList.desiredState = "up" + Mock Set-ASInstanceState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + It "Set-ASIS throws, Set-LBS also returns 'Fail'" { + $paramList.desiredState = "down" + Mock Set-ASInstanceState { throw } -ModuleName $moduleForMock + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState | should -be "Fail" + } + It "warns user that server is being pulled from LBs" { + $paramList.desiredState = "up" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -eq " : Changing aws LB state of host $($paramList.hostname) failed." } + } + It "warns user that server is being pulled from LBs" { + $paramList.desiredState = "down" + Set-LoadBalancerState -Server $paramList.hostname -DesiredState $paramList.desiredState + Assert-MockCalled Write-Warning -ModuleName $moduleForMock -ParameterFilter { $Message -eq " : Changing aws LB state of host $($paramList.hostname) failed." } + } + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.ps1 new file mode 100644 index 0000000..1ec0da8 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.ps1 @@ -0,0 +1,215 @@ +function Set-MicroserviceConfigurationBasedState { + +<# +.SYNOPSIS + Sets services to the desired state and starttype based on provider data and user input + +.DESCRIPTION + Sets services to the desired state and starttype based on provider data and user input. Running and manual when enabled + is specified, or stopped and disabled when disable is specified + +.PARAMETER ProviderData + Provider matching data used to determine services / packages to act upon + +.PARAMETER Enable + Enables and starts necessary services, when specified + +.PARAMETER Disable + Disables and stops unnecessary services, when specified + +.PARAMETER PrintDetailedResults + When specified, prints a detailed accounting of services acted upon + +.LINK + Disable-UnnecessaryMicroservices + Enable-NecessaryMicroservices + Get-ProviderMappingFileUnion +#> + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [object[]]$ProviderData, + + [Parameter(Mandatory=$true, ParameterSetName = "EnableServices")] + [switch]$Enable, + + [Parameter(Mandatory=$true, ParameterSetName = "DisableServices")] + [switch]$Disable, + + [Parameter(Mandatory = $false)] + [switch]$PrintDetailedResults + ) + + $logLead = Get-LogLeadName + + $notInstalledCount = 0 + $stateChangedCount = 0 + $tier0StateChangedCount = 0 + $invalidCount = 0 + $statusChangedCount = 0 + $tier0Services = Get-ServicesByTier -Tier 0 + + $serviceParams = if ($Enable.IsPresent) { + @{ + EnableAction = $true + DesiredState = "Running"; + ActionableState = "Stopped"; + DesiredStateVerb = "Started"; + DesiredStateActionVerb = "Starting"; + DesiredStartMode = "Manual"; + } + } else { + @{ + EnableAction = $false + DesiredState = "Stopped"; + ActionableState = "Running"; + DesiredStateVerb = "Stopped"; + DesiredStateActionVerb = "Stopping"; + DesiredStartMode = "Disabled"; + } + } + + # Choco list local packages. Exit early if not installed for speed purposes + # Saves a bunch of no-op service lookups by choco package name, which is slow + Write-Host "$logLead : Pulling local chocolatey packages" + $localChocos = (Get-ChocoState -LocalOnly).Name + + # Optional Detailed Output Driven by Switch + [PSObject[]]$detailedResults = @() + + # If any services fail to stop, we'll print them + [string[]]$serviceStateChangeFailures = @() + + # Track service count for tracking during execution + $serviceIndex = 1 + $totalProviderCount = $ProviderData.Count + + # Iterate over each unmatched provider and lookup the service, disable the service, and stop the service + foreach ($providerRecord in $ProviderData) { + $actionTakenOnService = $false + $chocoPackageName = $providerRecord.PackageId + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Looking for choco package $chocoPackageName" + + if ($localChocos -notcontains $chocoPackageName) { + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Chocolatey package $chocoPackageName is not installed locally. Skipping service lookup." + $notInstalledCount++ + $serviceIndex++ + continue + } + + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Looking for a service for choco package $chocoPackageName" + $svc = Get-ServiceByChocoName -ChocolateyName $chocoPackageName -IncludeDisabled -WarningAction SilentlyContinue + + if ($null -eq $svc) { + Write-Warning "$logLead : [$serviceIndex / $totalProviderCount] : Unable to find a matching service for package $chocoPackageName" + $invalidCount++ + $serviceIndex++ + continue + } + + # Tier 0 is hard coded to Automatic - Delayed -- SRE-17490 + if($tier0Services -Contains $chocoPackageName -and $serviceParams.DesiredStartMode -ne "Disabled") { + # Due to some shenanigans around Get-Service not properly returning information about Automatic vs Automatic-delayed, we are ALWAYS setting these tier 0 services to Automatic-delayed + # If this becomes an issue, we'll complicate Get-ServiceByChocoName, or do some complicated lookups here. + Write-Host ("$logLead : Service '{0}' is Tier0. Setting to Automatic-Delayed." -f $service.Name) + + $params = @("config", $service.Name, "start=delayed-auto") + Invoke-SCExe $params + + $tier0StateChangedCount++ + $actionTakenOnService = $true + } else { + if ($svc.StartMode -ne $serviceParams.DesiredStartMode) { + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Setting Service [$($svc.Name)] to $($serviceParams.DesiredStartMode)" + Set-Service -Name $svc.Name -StartupType $serviceParams.DesiredStartMode -WhatIf:$WhatIfPreference + $stateChangedCount++ + $actionTakenOnService = $true + } else { + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Service [$($svc.Name)] already has start mode set to $($serviceParams.DesiredStartMode)" + } + } + + if ($svc.State -eq $serviceParams.ActionableState) { + $actionTakenOnService = $true + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : $($serviceParams.DesiredStateActionVerb) service [$($svc.Name)]" + + if (-NOT $serviceParams.EnableAction) { + $actionResult = Stop-AlkamiService -ServiceName $svc.Name -WhatIf:$WhatIfPreference + } else { + # We should add SupportsShouldProcess but it doesn't have CmdletBinding. Riskier change, not including + # So here is a poor man's WhatIf. We can clean this up later + # TODO + # PSCmdlet.ShouldProcess is still valid, because this function SupportsShouldProcess + if ($PSCmdlet.ShouldProcess($svc.Name, "Start-AlkamiService")) { + + $actionResult = Start-AlkamiService -ServiceName $svc.Name + + } else { + + Write-Host "$logLead : Calling Start Alkami Service for [$($svc.Name)] (WhatIf)" + $actionResult = $true + } + } + + if ($true -eq $actionResult) { + $statusChangedCount++ + } else { + $serviceStateChangeFailures += $svc.Name + } + } else { + Write-Host "$logLead : [$serviceIndex / $totalProviderCount] : Service [$($svc.Name)] is not $($serviceParams.ActionableState) and cannot be set to $($serviceParams.DesiredState)" + } + + if ($PrintDetailedResults.IsPresent -and $actionTakenOnService) { + + $finalServiceRecord = Get-Service -Name $svc.Name + $detailedResults += New-Object PSObject -Property @{ + + ProviderName = $providerRecord.ProviderName + PackageName = $chocoPackageName + ServiceName = $svc.Name + OriginalServiceState = $svc.State + FinalServiceState = $finalServiceRecord.Status + OriginalServiceStartMode = $svc.StartMode + FinalServiceStartMode = $finalServiceRecord.StartType + } + } + + $serviceIndex++ + } + + Write-Host "$logLead : Operation complete" + + if ($WhatIfPreference) { + Write-Warning "$logLead : WhatIf Specified - No Action Taken" + } + + Write-Host "`t[$notInstalledCount] Packages Not Installed Locally" + Write-Host "`t[$invalidCount] Invalid Services" + Write-Host "`t[$stateChangedCount] Services Set to $($serviceParams.DesiredStartMode)" + Write-Host "`t[$tier0StateChangedCount] Tier 0 Services Set to Automatic-Delayed Start" + + if ($stateChangedCount -eq $statusChangedCount) { + Write-Host "`t[$statusChangedCount] Services $($serviceParams.DesiredStateVerb)" + } else { + Write-Host "`t[$statusChangedCount] Services $($serviceParams.DesiredStateVerb) (Variance Detected - Research Required)" -ForegroundColor Yellow + } + + if (-NOT (Test-IsCollectionNullOrEmpty -Collection $serviceStateChangeFailures)) { + Write-Host "`n" + Write-Warning "The below services failed to change state:" + [array]$serviceStateChangeFailures | ForEach-Object { Write-Host "`t$_" } + } + + if ($PrintDetailedResults.IsPresent) { + Write-Host "`nDetailed Results:" + foreach ($detailedResult in $detailedResults) { + + Write-Host "`t$($detailedResult.ProviderName) || $($detailedResult.PackageName)" + Write-Host "`tService: $($detailedResult.ServiceName)" + Write-Host "`tService Start Mode: $($detailedResult.OriginalServiceStartMode) => $($detailedResult.FinalServiceStartMode)" + Write-Host "`tService State: $($detailedResult.OriginalServiceState) => $($detailedResult.FinalServiceState)`n" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.tests.ps1 new file mode 100644 index 0000000..7ea8fcc --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-MicroserviceConfigurationBasedState.tests.ps1 @@ -0,0 +1,290 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-MicroserviceConfigurationBasedState" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { "[$sut (Pester)]" } + Mock -CommandName Stop-AlkamiService -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Start-AlkamiService -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-Service -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-Service -ModuleName $moduleForMock -MockWith {} + +#region $ProviderData Arragnement + + $unmatchedProviderA = New-Object PSObject -Property @{ + PackageId = "Pay2Win.Service.Host"; + ProviderName = "Pokemon Go Provider"; + ProviderType = "GottaCatchEmAll"; + } + + $unmatchedProviderB = New-Object PSObject -Property @{ + PackageId = "Unbearable.Temperatures"; + ProviderName = "HeatWaveProvider"; + ProviderType = "Texas"; + } + + $matchedProvderA = New-Object PSObject -Property @{ + PackageId = "SupaFakeChocoPackage"; + ProviderName = "Spectre/Meltdown CPU Thrashing Provider"; + ProviderType = "Malware"; + MatchCount = 1 + } + + $matchedProviderB = New-Object PSObject -Property @{ + PackageId = "Team.Fortress.2"; + ProviderName = "Scout is OP Provider"; + ProviderType = "Also Malware" + MatchCount = 2; + } + + $matchTestData = @( $matchedProvderA, $matchedProviderB) + $unmatchTestData = @( $unmatchedProviderA, $unmatchedProviderB) + +#endregion $ProviderData Arragnement + + Context "Enable Path" { + + It "Exits Early if the Package Is Not Installed" { + + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith {} + Set-MicroserviceConfigurationBasedState -ProviderData $matchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Times 0 -Exactly -Scope It + } + + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + + return @( + @{ + Name = "SupaFakeChocoPackage"; + Version = "1.0.0"; + }, + @{ + Name = "Team.Fortress.2"; + VErsion = "2.9.9"; + } + ) + } + + It "Exits Early and Writes a Warning if the Package is Installed but Service Does Not Exist" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Set-MicroserviceConfigurationBasedState -ProviderData $matchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 1 -Exactly -Scope It -ParameterFilter { + $ChocolateyName -eq "SupaFakeChocoPackage" + } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 1 -Exactly -Scope It -ParameterFilter { + $ChocolateyName -eq "Team.Fortress.2" + } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 2 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Unable to find a matching service for" } + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "SupaFakeChocoPackage"} -MockWith { + + return New-Object PSObject -Property @{ + ProcessId = 1; + Name = "Alkami.CPU.Thrasher.Service.Host"; + StartMode = "Disabled"; + State = "Stopped"; + Status = "OK"; + ExitCode = "999"; + } + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "Team.Fortress.2"} -MockWith { + + return New-Object PSObject -Property @{ + ProcessId = 90; + Name = "Alkami.Heavy.Is.Trash.Service.Host"; + StartMode = "Manual"; + State = "Running"; + Status = "OK"; + ExitCode = "555"; + } + } + + It "Only Sets Service StartMode if the Service is Not Already Enabled" { + + Set-MicroserviceConfigurationBasedState -ProviderData $matchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 1 -Exactly -Scope It + } + + It "Only Sets Service State if the Service is Not Already Running" { + + Set-MicroserviceConfigurationBasedState -ProviderData $matchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Start-AlkamiService -Times 1 -Exactly -Scope It + } + + It "Writes a Warning if there is a Variance between Enabled and Started Services" { + + Mock -CommandName Start-AlkamiService -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Set-MicroserviceConfigurationBasedState -ProviderData $matchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "The below services failed to change state:" } + } + } + + Context "Disable Path" { + + It "Exits Early if the Package Is Not Installed" { + + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith {} + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Times 0 -Exactly -Scope It + } + + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + + return @( + @{ + Name = "Unbearable.Temperatures"; + Version = "1.0.0"; + }, + @{ + Name = "Pay2Win.Service.Host"; + VErsion = "2.9.9"; + } + ) + } + + It "Exits Early and Writes a Warning if the Package is Installed but Service Does Not Exist" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 2 -Exactly -Scope It + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 2 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Unable to find a matching service for" } + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "Pay2Win.Service.Host"} -MockWith { + + return New-Object PSObject -Property @{ + ProcessId = 9000; + Name = "Pokemon Go Griftery Store MicroService"; + StartMode = "Disabled"; + State = "Stopped"; + Status = "OK"; + ExitCode = "999"; + } + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "Unbearable.Temperatures"} -MockWith { + + return New-Object PSObject -Property @{ + ProcessId = 456; + Name = "Break.Yo.Thermometer.Service.Host"; + StartMode = "Manual"; + State = "Running"; + Status = "OK"; + ExitCode = "555"; + } + } + + It "Only Sets Service StartMode if the Service is Not Already Disabled" { + + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 1 -Exactly -Scope It + } + + It "Only Stops a Service if the Service is Not Already Stopped" { + + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Times 1 -Exactly -Scope It + } + + It "Writes a Warning if there is a Variance between Disabled and Stopped Services" { + + Mock -CommandName Stop-AlkamiService -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "The below services failed to change state" } + } + } + + Context "Tier 0 Path" { + $unmatchTestData = @( $unmatchedProviderA) + + Mock -CommandName Get-ServicesByTier -ModuleName $moduleForMock -MockWith{ + return @( + "Pay2Win.Service.Host" + ) + } + + Mock -CommandName Get-ChocoState -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = "Pay2Win.Service.Host"; + Version = "2.9.9"; + } + ) + } + + Mock -CommandName Invoke-SCExe -ModuleName $moduleForMock -MockWith { + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "Pay2Win.Service.Host"} -MockWith { + return New-Object PSObject -Property @{ + ProcessId = 9000; + Name = "Pokemon Go Griftery Store MicroService"; + StartMode = "Manual"; + State = "Running"; + Status = "OK"; + ExitCode = "999"; + } + } + + It "Sets Service StartMode if the Service is Not Already Disabled" { + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 1 -Exactly -Scope It + } + + It "Sets Service StartMode via Invoke-SCExe if the Service is Not Already Automatic" { + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Enable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It + } + + Mock -CommandName Get-ServiceByChocoName -ModuleName $moduleForMock -ParameterFilter {$ChocolateyName -eq "Pay2Win.Service.Host"} -MockWith { + return New-Object PSObject -Property @{ + ProcessId = 9000; + Name = "Pokemon Go Griftery Store MicroService"; + StartMode = "Disabled"; + State = "Stopped"; + Status = "OK"; + ExitCode = "999"; + } + } + + It "DOES NOT call Set-Service for a Tier 0 service" { + Set-MicroserviceConfigurationBasedState -ProviderData $unmatchTestData -Disable + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ServiceByChocoName -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.ps1 new file mode 100644 index 0000000..39ec3e4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.ps1 @@ -0,0 +1,101 @@ +function Set-NagAlerts { + <# +.SYNOPSIS +Disables or Enables Nag Alerts by Tweaking the Job Whitelist. Restarts Nag afterwards + +.DESCRIPTION +Disable or Enables Nag Alerts by Tweaking the Job Whitelist. Restarts Nag afterwards +See also: https://confluence.alkami.com/display/SRE/Disable+NAG+Alerts + +.PARAMETER nagPath +The path where Nag is installed. Defaults to (Get-OrbPath)\Nag + +.PARAMETER noRestart +Skips recycling Nag + +.PARAMETER enable +Enables Nag Alerts + +.PARAMETER disable +Disables Nag Alerts + +#> + [CmdletBinding()] + param( + [Parameter(ParameterSetName = 'EnableAlertsParameterSet', Mandatory = $false)] + [Parameter(ParameterSetName = 'DisableAlertsParameterSet', Mandatory = $false)] + [Parameter(Mandatory = $false)] + [string]$nagPath, + + [Parameter(ParameterSetName = 'EnableAlertsParameterSet', Mandatory = $false)] + [Parameter(ParameterSetName = 'DisableAlertsParameterSet', Mandatory = $false)] + [Parameter(Mandatory = $false)] + [switch]$noRestart, + + [Parameter(ParameterSetName = 'EnableAlertsParameterSet', Mandatory = $true)] + [Parameter(Mandatory = $false)] + [Alias("Enabled")] + [switch]$enable, + + [Parameter(ParameterSetName = 'DisableAlertsParameterSet', Mandatory = $true)] + [Parameter(Mandatory = $false)] + [Alias("Disabled")] + [switch]$disable + ) + + $logLead = (Get-LogLeadName); + + if ([string]::IsNullOrEmpty($nagPath)) { + $nagPath = (Join-Path(Get-OrbPath) "\Nag") + } + + Write-Verbose "$logLead : Using Nag Path $nagPath" + + $nagService = Get-Service "Alkami Nag Service" -ErrorAction SilentlyContinue + + if ($null -eq $nagService -or $nagService.StartMode -eq "Disabled") { + Write-Warning "$logLead : The Nag Service is not installed or is disabled on this machine." + } + + $nagConfig = (Join-Path $nagPath "Alkami.App.Nag.Host.Service.exe.config") + + if (!(Test-Path $nagConfig)) { + + Write-Warning "$logLead : Could not find the Nag config file at $nagConfig. Execution cannot continue" + return + } + + # ToDo: When dev changes the app setting key in ORB we'll remove the term WhiteList from this function + # Until then, we will match the development code base to avoid any confusion + # See: SRE-14133 + $currentJobWhitelistValue = Get-AppSetting -key 'JobNameWhitelistRegex' -FilePath $nagConfig + + if ($enable.IsPresent) { + + $enabledJobWhitelistValue = "" + if ($currentJobWhitelistValue -eq $enabledJobWhitelistValue) { + + Write-Warning "$logLead : App setting already has the correct value. Exiting function" + return + } + + Set-AppSetting -key 'JobNameWhitelistRegex' -value $enabledJobWhitelistValue -filePath $nagConfig + + } elseif ($disable.IsPresent) { + + $disabledJobWhitelistValue = "(?i)Scheduled.*" + + if ($currentJobWhitelistValue -eq $disabledJobWhitelistValue) { + + Write-Warning "$logLead : App setting already has the correct value. Exiting function" + return + } + + Set-AppSetting -key 'JobNameWhitelistRegex' -value $disabledJobWhitelistValue -filePath $nagConfig + } + + if (!($noRestart.IsPresent)) { + + Restart-Nag + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.tests.ps1 new file mode 100644 index 0000000..31d08b6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-NagAlerts.tests.ps1 @@ -0,0 +1,126 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +function Save-EnabledConfiguration { + param( + [string]$filePath + ) + @" + + + + + + +"@ | Out-File $filePath -Force +} + +function Save-DisabledConfiguration { + param( + [string]$filePath + ) + @" + + + + + + +"@ | Out-File $filePath -Force +} + +Describe "Set-NagAlerts" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Warning ("Using temp path: $tempPath for tests") + + $tempConfig = (Join-Path $tempPath "Alkami.App.Nag.Host.Service.exe.config") + + Context "Parameter Validation" { + + Write-Warning $tempConfig + It "Does Not Run if -Enable or -Disable Are Not Provided" { + + { Set-NagAlerts } | Should -Throw + } + + It "Does Not Run if Both -Enable and -Disable Are Provided" { + + { Set-NagAlerts -Enable -Disable } | Should -Throw + } + + It "Does Not Restart Nag if the -noRestart Switch is Provided" { + + Mock Restart-Nag -ModuleName $moduleForMock -MockWith {} + Mock Get-Service -ModuleName $moduleForMock -MockWith {} + + Save-DisabledConfiguration $tempConfig + Set-NagAlerts -Enabled -nagPath $tempPath -noRestart + Assert-MockCalled -ModuleName $moduleForMock Restart-Nag -Times 0 -Exactly -Scope It + } + } + + Context "File Manipulation" { + + It "Disables Alerts When the Disable Switch is Provided and Alerts are Enabled" { + + Mock Restart-Nag -ModuleName $moduleForMock -MockWith {} + Mock Get-Service -ModuleName $moduleForMock -MockWith {} + + # Save a config with alerts enabled + Save-EnabledConfiguration $tempConfig + Set-NagAlerts -Disabled -nagPath $tempPath + Get-AppSetting -FilePath $tempConfig -Key "JobNameWhitelistRegex" | Should -Be "(?i)Scheduled.*" + Assert-MockCalled -ModuleName $moduleForMock Restart-Nag -Times 1 -Exactly -Scope It + } + + It "Enables Alerts When the Enable Switch is Provided and Alerts are Disabled" { + + Mock Restart-Nag -ModuleName $moduleForMock -MockWith {} + Mock Get-Service -ModuleName $moduleForMock -MockWith {} + + # Save a config with alerts enabled + Save-DisabledConfiguration $tempConfig + Set-NagAlerts -Enabled -nagPath $tempPath + Get-AppSetting -FilePath $tempConfig -Key "JobNameWhitelistRegex" | Should -Be "" + Assert-MockCalled -ModuleName $moduleForMock Restart-Nag -Times 1 -Exactly -Scope It + } + + It "Does Not Edit the File or Restart Nag When the Disable Switch is Provided and Alerts are Disabled" { + + Mock Restart-Nag -ModuleName $moduleForMock -MockWith {} + Mock Get-Service -ModuleName $moduleForMock -MockWith {} + Mock Set-AppSetting -ModuleName $moduleForMock -MockWith { Write-Warning "Mock Called!" } + + # Save a config with alerts enabled + Save-DisabledConfiguration $tempConfig + Set-NagAlerts -Disabled -nagPath $tempPath + Get-AppSetting -FilePath $tempConfig -Key "JobNameWhitelistRegex" | Should -Be "(?i)Scheduled.*" + Assert-MockCalled -ModuleName $moduleForMock Set-AppSetting -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock Restart-Nag -Times 0 -Exactly -Scope It + } + + It "Does Not Edit the File or Restart Nag When the Enable Switch is Provided and Alerts are Enabled" { + + Mock Restart-Nag -ModuleName $moduleForMock -MockWith {} + Mock Get-Service -ModuleName $moduleForMock -MockWith {} + Mock Set-AppSetting -ModuleName $moduleForMock -MockWith { Write-Warning "Mock Called!" } + + # Save a config with alerts enabled + Save-EnabledConfiguration $tempConfig + Set-NagAlerts -Enabled -nagPath $tempPath + Get-AppSetting -FilePath $tempConfig -Key "JobNameWhitelistRegex" | Should -Be "" + Assert-MockCalled -ModuleName $moduleForMock Set-AppSetting -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock Restart-Nag -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.Tests.ps1 new file mode 100644 index 0000000..4d49a71 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.Tests.ps1 @@ -0,0 +1,201 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +#Write-Host "Overriding SUT: $functionPath" +#Import-Module $functionPath -Force +$moduleForMock = "" +$moduleForMock = "Alkami.DevOps.Operations" +# Handle the AWS Native Functions When AWSPowerShell Not Available +# Import-AWSModule # SSM +if ($null -eq (Get-Command "Get-SSMParameter" -ErrorAction SilentlyContinue)) { + function Get-SSMParameter { + + throw "This Should Never Be Actually Called" + } + +} +Describe "Set-NginxHostState" { + + Mock Get-SSMParameter -ModuleName $moduleForMock { + $body = New-Object psobject -Property @{ + Value = "foo" + } + return $body + } + + Mock Test-IsAws -ModuleName $moduleForMock { return $true } + + Context "Writes error when NGINX instances are not found." { + + Mock Get-NginxIpAddresses -ModuleName $moduleForMock { + return $null + } + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} -Verifiable + + $result = ( Set-NginxHostState -desiredState "up" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" ) + + It "Returns correct result" { + $result | Should -be "Fail" + } + + It "Wrote an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + } + + Context "Writes error when NGINX is unreachable" { + + Mock Get-NginxIpAddresses -ModuleName $moduleForMock { + # need address that will always resolve and is pingable + $mockedNginxInstanceIp = (Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + return @( $mockedNginxInstanceIp ) + } + + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} -Verifiable + + Mock Test-TcpConnection -ModuleName $moduleForMock { + return $false + } + + $result = ( Set-NginxHostState -desiredState "up" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" ) + + It "Returns correct result" { + $result | Should -be "Fail" + } + + It "Wrote an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + } + + + Context "peer is in up state" { + + Mock Get-NginxIpAddresses -ModuleName $moduleForMock { + # need address that will always resolve and is pingable + #$mockedNginxInstanceIp = (Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + $mockedNginxInstanceIp = Get-IpAddress + return @( $mockedNginxInstanceIp ) + } + + Mock Test-TcpConnection -ModuleName $moduleForMock { + return $true + } + + Mock Invoke-RestMethod -ModuleName $moduleForMock -ParameterFilter { $method -eq 'GET' } { + $fakeServerIP = Get-IpAddress #(Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + $body = New-Object psobject -Property @{ + "FakeUpstream.com-ssl" = @{ + "zone" = "FakeUpstream.com-ssl" + "peers" = @{ + id = 0 + server = "$($fakeServerIP):443" + name = "$($fakeServerIP):443" + state = "up" + } + } + } + return $body + } + + Mock Invoke-RestMethod -ModuleName $moduleForMock -ParameterFilter { $method -eq 'PATCH' } { + $fakeServerIP = Get-IpAddress #(Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + $body = New-Object psobject -Property @{ + id = 0 + server = "$($fakeServerIP):443" + down = "true" + weight = 1 + max_conns = 0 + max_fails = 100 + fail_timeout = "10s" + slow_start = "0s" + route = "" + backup = "False" + } + return $body + } + + It "Asserts GET-SSMParameter mock is working ." { + $true | Should -BeTrue -Because "THIS TEST SHOULD NOT BE HERE" + (Get-NginxAuthHeaders).Authorization | Should -Be (@{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("foo:foo")) }).Authorization + } + + It "Asserts Server is already in the desired state and returns NoOp." { + Set-NginxHostState -desiredState "up" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" | Should -be "NoOp" + } + + It "Asserts Server is already in the desired state and forces to continue" { + Set-NginxHostState -desiredState "up" -targetEnvironment "prod" -hostname "localhost" -force | Should -Be "Success" + } + + It "Asserts server is put down" { + Set-NginxHostState -desiredState "down" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" | Should -be "Success" + } + + It "Asserts Server is already in the desired state as prod targetEnvironment" { + Set-NginxHostState -desiredState "up" -targetEnvironment "prod" -hostname "localhost" -ErrorAction "Stop" | Should -be "NoOp" + } + } + + Context "peer is in down state" { + + Mock Get-NginxIpAddresses -ModuleName $moduleForMock { + # need address that will always resolve and is pingable + $mockedNginxInstanceIp = Get-IpAddress #(Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + return @( $mockedNginxInstanceIp ) + } + + Mock Test-TcpConnection -ModuleName $moduleForMock { + return $true + } + + Mock Invoke-RestMethod -ModuleName $moduleForMock -ParameterFilter { $method -eq 'GET' } { + $fakeServerIP = Get-IpAddress #(Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + $body = New-Object psobject -Property @{ + "FakeUpstream.com-ssl" = @{ + "zone" = "FakeUpstream.com-ssl" + "peers" = @{ + id = 0 + server = "$($fakeServerIP):443" + name = "$($fakeServerIP):443" + state = "down" + } + } + } + return $body + } + + Mock Invoke-RestMethod -ModuleName $moduleForMock -ParameterFilter { $method -eq 'PATCH' } { + $fakeServerIP = Get-IpAddress #(Resolve-DnsName -Name "localhost" -Type A -ErrorAction SilentlyContinue).IpAddress + $body = New-Object psobject -Property @{ + id = 0 + server = "$($fakeServerIP):443" + down = "false" + weight = 1 + max_conns = 0 + max_fails = 100 + fail_timeout = "10s" + slow_start = "0s" + route = "" + backup = "False" + } + return $body + } + + It "Asserts Server is already in the desired state and throws." { + Set-NginxHostState -desiredState "down" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" | Should -be "NoOp" + } + + It "Asserts Server is already in the desired state and forces to continue" { + Set-NginxHostState -desiredState "down" -targetEnvironment "staging" -hostname "localhost" -force -ErrorAction "Stop" | Should -Be "Success" + } + It "Asserts server is put down" { + Set-NginxHostState -desiredState "up" -targetEnvironment "staging" -hostname "localhost" -ErrorAction "Stop" | Should -be "Success" + } + It "Asserts Server is already in the desired state as prod targetEnvironment" { + Set-NginxHostState -desiredState "down" -targetEnvironment "prod" -hostname "localhost" -ErrorAction "Stop" | Should -be "NoOp" + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.ps1 new file mode 100644 index 0000000..10d2ed6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-NginxHostState.ps1 @@ -0,0 +1,156 @@ +function Set-NginxHostState { +<# +.Synopsis + Switch the current state of a host in a supplied set of Nginx servers. + +.Parameter desiredState + Required. Must be "down" or "up". Specifies which state to put the Host into. + +.Parameter force + Optional. If supplied, this will force the state to change to the specified desiredState. This could be problematic as it will eat Errors in the case of a host being in an unexpected state. + +.Parameter targetEnvironment + Required. Must be "staging" or "prod" + +.Parameter hostname + Requird. Hostname to toggle "webxxxxx" + +.PARAMETER AwsProfileName + [string] Specific AWS CLI Profile to use in AWS API calls. + +.PARAMETER AwsRegion + [string] Specific AWS CLI Region to use in AWS API calls. + +.OUTPUTS + Returns "Fail" on failing conditions +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $true)] + [ValidateSet("down", "up")] + [string]$desiredState, + + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + + [Parameter(Mandatory = $false)] + [string]$targetEnvironment = $null, + + [Parameter(Mandatory = $false)] + [switch]$force, + + [Parameter(Mandatory = $false)] + [string]$AwsProfileName, + + [Parameter(Mandatory = $false)] + [string]$AwsRegion + ) + + $logLead = (Get-LogLeadName) + + if ($hostname -eq "localhost") { + $alkHostIP = Get-IpAddress + } else { + # attempt to resolve host to toggle + # Special ErrorAction circumstance - don't try this at kids, home - Really, don't normally do this + $alkHostIP = (Resolve-DnsName -Name $hostname -Type A -ErrorAction SilentlyContinue).IpAddress + } + if ($null -eq $alkHostIP ) { + Write-Error "$logLead : unable to resolve IP address of $hostname" + return "Fail" + } +# } + + # If the target environment isn't specified, construct it from the alk:env tag in the environment. + if([string]::IsNullOrWhiteSpace($targetEnvironment)) { + try { + $instances = (Get-EC2InstancesByHostname -Servers $hostname -ProfileName $awsProfileName) + $targetEnvironment = $instances[0].Tags | ` Where-Object {$_.Key -eq 'alk:env'} | Select-Object -ExpandProperty "Value" + } catch { + Write-Warning "$loglead : Exception occurred getting environment from AWS." + Write-Error $_.Exception.Message + return "Fail" + } + } + + # Get Nginx server IP addresses + $nginxIpAddresses = Get-NginxIpAddresses -TargetEnvironment $targetEnvironment -AwsProfileName $AwsProfileName -AwsRegion $AwsRegion + if (Test-IsCollectionNullOrEmpty -collection $nginxIpAddresses) { + Write-Error "$logLead : no Nginx Servers found" + return "Fail" + } + + Write-Verbose "nginx servers $nginxIpAddresses " + + # Pre-flight the NGINX server connectivity. We don't want to proceed unless they are all available. + foreach ($nginxIpAddress in $nginxIpAddresses) { + $tcpResult = Test-TcpConnection -ipAddress $nginxIpAddress -port 8080 -msTimeout 5000 + if ($false -eq $tcpResult) { + # If TCP test fails, we do not want to proceed. NGINX servers may become out of sync + Write-Error "$logLead : This NGINX server was unreachable $nginxIpAddress" + return "Fail" + } + } + + foreach ($nginxIpAddress in $nginxIpAddresses) { + + # gets the current state + $getResults = Get-NginxUpstreams -NginxServer $nginxIpAddress -targetEnvironment $targetEnvironment -ProfileName $AwsProfileName -Region $AwsRegion + + # If getting the upstreams fails, but ping succeded, nginx may become out of sync + if (!$getResults) { + Write-Error "$logLead : Getting upstreams failed." + break + } + + $resultsHash = $getResults | ConvertTo-Json -Depth 4 | ConvertFrom-JsonToHashtable + + foreach ($serviceName in $resultsHash.Keys) { + Write-Verbose "$logLead : Looking for $hostname in ServiceName : $serviceName" + foreach ($peer in $resultsHash["$serviceName"]["peers"]) { + Write-Verbose "$logLead : Looking for $hostname in Peer : $($peer.name)" + + Write-Verbose "$logLead : Testing if $($peer["server"]) matches '$($alkHostIP):*'" + #The $($varName) format is necessary to put a ":" after a variable, otherwise it thinks it's a scope variable + #Like $Global:varname + #Alternate matcher: '-match "^$($alkHostIp):"' + if ($peer["server"] -like "$($alkHostIp):*") { + $uri = "http://$($nginxIpAddress):8080/api/3/http/upstreams/$serviceName/servers/$($peer["id"])" + + if (($peer["state"] -like $desiredState) -and (!$force.IsPresent)) { + Write-Warning "$logLead : $serviceName is not in the expected state: Not $desiredState" + return "NoOp" + } elseif (($peer["state"] -like $desiredState) -and ($force.IsPresent)) { + Write-Warning "$logLead : $serviceName is already in the desired state. Continuing anyway..." + $PatchResult = Set-NginxUpstreamHost -Uri $uri -targetEnvironment $targetEnvironment -desiredState $desiredState -ProfileName $AwsProfileName -Region $AwsRegion + } else { + $PatchResult = Set-NginxUpstreamHost -Uri $uri -targetEnvironment $targetEnvironment -desiredState $desiredState -ProfileName $AwsProfileName -Region $AwsRegion + } + + if ($peer["state"] -notlike $PatchResult.down -and ($null -ne $PatchResult.down)) { + Write-Verbose "patch result" + $patchResult | Write-Verbose + Write-Verbose "$logLead : Service: $serviceName for host: $hostname state is now: down=$($patchResult.down)" + Write-Host "$logLead : State changed succesfully" + $statusToReturn = "success" + } elseif (($peer["state"] -like $PatchResult.down) -and ($force.IsPresent)) { + Write-Verbose "patch result" + $patchResult | Write-Verbose + Write-Verbose "$logLead : Service: $serviceName for host: $hostname state is still: down=$($patchResult.down)" + Write-Host "$logLead : State was forced and stayed the same" + $statusToReturn = "success" + } else { + Write-Warning "$logLead : State was not changed Succesfully, Nginx requires review" + return "Fail" + } + } + } + } + if (!$uri) { + Write-Error "$logLead : $($hostname) $alkhostip was not found in upstreams queried " + return "Fail" + } + } + return $statusToReturn +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-NginxUpstreamHost.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-NginxUpstreamHost.ps1 new file mode 100644 index 0000000..4f4ca0f --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-NginxUpstreamHost.ps1 @@ -0,0 +1,62 @@ +function Set-NginxUpstreamHost { + <# +.SYNOPSIS + Set the state of the host at the given URI to a specific state, up or down. + +.PARAMETER Uri + Nginx server API URI to set Upstream Host State + +.PARAMETER TargetEnvironment + Target environment of host to update, staging or prod + +.PARAMETER DesiredState + Desired end state of host to change, up or down + +.PARAMETER ProfileName + AWS ProfileName + +.PARAMETER Region + AWS Region + +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Uri, + + [Parameter(Mandatory = $true)] + [ValidateSet("staging", "prod")] + [string]$TargetEnvironment, + + [Parameter(Mandatory = $true)] + [ValidateSet("down", "up")] + [string]$DesiredState, + + [Parameter(Mandatory = $false)] + [string]$ProfileName, + + [Parameter(Mandatory = $false)] + [string]$Region + ) + + $logLead = Get-LogLeadName + + if ($TargetEnvironment -eq "staging") { + $getAuthHeaders = Get-NginxAuthHeaders -ProfileName $ProfileName -Region $Region + } + + # We need the "down" value state as the JSON Body + if ($DesiredState -eq "down") { + $downVal = @{"down" = $true } | ConvertTo-Json + } else { + $downVal = @{"down" = $false } | ConvertTo-Json + } + + Write-Host "$logLead : Making request against: $Uri" + Write-Host "$logLead : Setting to desired state: $DesiredState" + $results = Invoke-RestMethod -Uri $Uri -Headers $getAuthHeaders -Body $downVal -Method 'PATCH' -UseBasicParsing + if ($null -eq $results) { + Write-Error "$logLead : Unable to set $DesiredState at $Uri" + } + return $results +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.ps1 new file mode 100644 index 0000000..645d6f4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.ps1 @@ -0,0 +1,150 @@ +function Set-Route53HostedZoneVpcAssocations { + +<# +.SYNOPSIS + Establish VPC associations for all Alkami VPCs with a set of Route53 Hosted Zones. + +.DESCRIPTION + Establish VPC associations for all Alkami VPCs with a set of Route53 Hosted Zones. By default, this function + will process all Alkami Route53 Hosted Zones; use parameters to limit the scope of the change if desired. + + Note that this function will attempt to register all Alkami VPCs with the newly created zone; to facilitate that process, + call this function only if you have current AWS CLI credentials in each Alkami account. + + If you only want to associate a single VPC with a single Route53 Hosted Zone, prefer 'Add-Route53HostedZoneVpcAssociation'. + +.PARAMETER TargetProfile + [string] Limit Route53 Hosted Zone operations to a single AWS account. Required if the 'TargetHostedZoneId' parameter is provided. + +.PARAMETER TargetHostedZoneId + [string] Limit Route53 Hosted Zone operations to a single Route53 Hosted Zone. + +.EXAMPLE + Set-Route53HostedZoneVpcAssocations -TargetProfile 'temp-sandbox' -TargetHostedZoneId 'Z097623831BHXFFO7D74E' + +[Add-Route53HostedZoneVpcAssociation]: Creating VPC 'vpc-f7cc3a8c' association authorization for Route53 Hosted Zone '/hostedzone/Z097623831BHXFFO7D74E' + +#> + + [CmdletBinding(DefaultParameterSetName='TargetProfile')] + param ( + [Parameter( Mandatory = $false, ParameterSetName='TargetProfile' )] + [Parameter( Mandatory = $true, ParameterSetName='TargetZone' )] + [ValidateNotNullOrEmpty()] + [string] $TargetProfile, + + [Parameter( Mandatory = $true, ParameterSetName='TargetZone' )] + [ValidateNotNullOrEmpty()] + [string] $TargetHostedZoneId + ) + + [string] $logLead = (Get-LogLeadName) + [object[]] $totalVpcList = @() + [string[]] $vpcRegions = @( "us-east-1", "us-west-2" ) + + # Loop through each profile and region and pull the VPC IDs for each. + Write-Verbose "$logLead : Retrieving all VPC IDs for Alkami." + [string[]] $vpcProfileNameList = Get-AlkamiAwsProfileList + foreach ( $profile in $vpcProfileNameList ) { + + foreach ( $region in $VpcRegions ) { + + [string[]] $curVpcList = Get-VpcIdList -ProfileName $profile -Region $region + + foreach ( $vpcEntry in $curVpcList ) { + + $totalVpcList += New-Object PSObject -Property @{ + ID = $vpcEntry + Region = $region + Profile = $profile + } + } + } + } + + # Sanity check that there are VPCs at Alkami. + if ( Test-IsCollectionNullOrEmpty $totalVpcList ) { + + Write-Error "There are no VPCs in the entirety of AWS for Alkami. Something has gone horribly wrong (or your credentials are invalid)." + return + } + + Write-Verbose ( "{0} : Discovered {1} Alkami VPCs." -f $logLead, $totalVpcList.Count ) + + $vpcAssociationScript = { + param( + [object] $vpcConfiguration, + [string[]] $inputArguments + ) + + $scriptResult = New-Object -TypeName PSObject -Property @{ + HostedZoneId = $inputArguments[0] + HostedZoneProfile = $inputArguments[1] + VpcId = $vpcConfiguration.ID + VpcRegion = $vpcConfiguration.Region + VpcProfile = $vpcConfiguration.Profile + Success = $false + } + + try { + + $scriptResult.Success = ( Add-Route53HostedZoneVpcAssociation -ZoneId $inputArguments[0] -ZoneProfileName $inputArguments[1] ` + -VpcId $vpcConfiguration.ID -VpcRegion $vpcConfiguration.Region ` + -VpcProfileName $vpcConfiguration.Profile ) + + } catch { + + Write-Warning "$logLead : $_" + } + + return $scriptResult + } + + # If the user provided a target profile, use it. By default, we process all profiles. + if ( $PSBoundParameters.ContainsKey( 'TargetProfile' ) ) { + + [string[]] $targetHostedZoneProfileList = @( $TargetProfile ) + + } else { + + [string[]] $targetHostedZoneProfileList = $vpcProfileNameList + } + + # Loop through each profile and pull the Route 53 Hosted Zones + foreach ( $profile in $targetHostedZoneProfileList ) { + + [string[]] $hostedZoneList = Get-Route53HostedZoneIdList -ProfileName $profile -PrivacyStatusFilter $true + + foreach ( $hostedZone in $hostedZoneList ) { + + # Determine if we're targeting a specific hosted zone. + # - If we aren't, process all zones. + # - If we are, is this the droid we're looking for? + if ( ( $false -eq $PSBoundParameters.ContainsKey( 'TargetHostedZoneId' ) ) -or ( $hostedZone -match $TargetHostedZoneId ) ) { + + # Process the VPC associations for the current hosted zone. + Write-Verbose "$logLead : Processing Route53 Hosted Zone ID '$hostedZone' using profile '$profile'." + $scriptResultList = Invoke-Parallel -objects $totalVpcList -script $vpcAssociationScript -arguments @( $hostedZone, $profile ) + + # Process any failed results. Note that we don't fail the job since theoretically some of the associations might have worked. + $failedScriptResultList = $scriptResultList | Where-Object { $false -eq $_.Success } + foreach ( $failedResult in $failedScriptResultList ) { + + Write-Warning ( "{0} : Failed to associate VPC ID '{1}' using profile '{2}' and region '{3}' with Route 53 Hosted Zone ID '{4}' using profile '{5}'" -f ` + $logLead, $failedResult.VpcId, $failedResult.VpcProfile, $failedResult.VpcRegion, $failedResult.HostedZoneId, $failedResult.HostedZoneProfile ) + } + + # Look for any stale authorizations that may be languishing for this Route 53 hosted zone and remove them. + # These can be left behind if credentials expire mid-association. Technically, these don't cause any harm + # to leave (and you can create the association authorization even if it pre-exists), but we should try to clean + # up after ourselves because there IS a quota limit on the number of VPC association authorizations. + # Ref: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html + $staleAuthorizations = ( Get-R53VPCAssociationAuthorizationList -ProfileName $profile -HostedZoneId $hostedZone ).VPCs + foreach ( $auth in $staleAuthorizations ) { + + Remove-R53VPCAssociationAuthorization -ProfileName $profile -HostedZoneId $hostedZone -VPC_VPCId $auth.VPCId -VPC_VPCRegion $auth.VPCRegion -Force + } + } + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.tests.ps1 new file mode 100644 index 0000000..f197779 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-Route53HostedZoneVpcAssocations.tests.ps1 @@ -0,0 +1,209 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 + +Describe "Set-Route53HostedZoneVpcAssocations" { + + Mock -CommandName Get-LogLeadName -ModuleName Alkami.DevOps.Operations -MockWith { return 'Set-Route53HostedZoneVpcAssocations.tests' } + + Context "Input Validation" { + + It "Target Profile Name Should Not Be Null" { + + { Set-Route53HostedZoneVpcAssocations -TargetProfile $null } | Should -Throw + } + + It "Target Profile Name Should Not Be Empty" { + + { Set-Route53HostedZoneVpcAssocations -TargetProfile '' } | Should -Throw + } + + It "Target Hosted Zone ID Should Not Be Null" { + + { Set-Route53HostedZoneVpcAssocations -TargetProfile 'Test' -TargetHostedZoneId $null } | Should -Throw + } + + It "Target Hosted Zone ID Should Not Be Empty" { + + { Set-Route53HostedZoneVpcAssocations -TargetProfile 'Test' -TargetHostedZoneId '' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Write Error If No VPCs Exist" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return $null } + Mock -CommandName Write-Error -ModuleName Alkami.DevOps.Operations -MockWith {} + + Set-Route53HostedZoneVpcAssocations | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- one profile, two regions. + # - Write-Error: Called once if there are no VPCs. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations ` + -ParameterFilter { $Message -match "There are no VPCs" } + + } + + It "Should Process All Alkami Profile Hosted Zones If Target Profile Not Supplied" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $true } ) } + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { return @() } + + Set-Route53HostedZoneVpcAssocations | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- two profiles, two hosted zones. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- two profiles, two hosted zones. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Process Only Target Profile Hosted Zones If Target Profile Supplied" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $true } ) } + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { return @() } + + Set-Route53HostedZoneVpcAssocations -TargetProfile 'Test1' | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile -- one profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- one profile, two hosted zones. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- one profile, two hosted zones. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Process Only Target Hosted Zone If Target Hosted Zone Supplied" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $true } ) } + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { return @() } + + Set-Route53HostedZoneVpcAssocations -TargetProfile 'Test1' -TargetHostedZoneId 'Test1' | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile -- one profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- one profile, one hosted zone. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- one profile, one hosted zone. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Process No Hosted Zones If Supplied Target Hosted Zone Not Found" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $true } ) } + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { return @() } + + Set-Route53HostedZoneVpcAssocations -TargetProfile 'Test1' -TargetHostedZoneId 'Test3' | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile -- one profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- one profile, zero hosted zones. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- one profile, zero hosted zones. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 0 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 0 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Write Warning If VPC Associations Fail" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $false }, @{ 'Success' = $true } ) } + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { return @() } + Mock -CommandName Write-Warning -ModuleName Alkami.DevOps.Operations -MockWith {} + + Set-Route53HostedZoneVpcAssocations | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- two profiles, two hosted zones. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- two profiles, two hosted zones. + # - Write-Warning: Called once per profile per hosted zone per failure -- two profiles, two hosted zones, one failure. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Write-Warning -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + + It "Should Remove Stale VPC Association Authorizations If Exist" { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Get-VpcIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test' ) } + Mock -CommandName Get-Route53HostedZoneIdList -ModuleName Alkami.DevOps.Operations -MockWith { return @( 'Test1', 'Test2' ) } + Mock -CommandName Invoke-Parallel -ModuleName Alkami.DevOps.Operations -MockWith { return @( @{ 'Success' = $true } ) } + Mock -CommandName Remove-R53VPCAssociationAuthorization -ModuleName Alkami.DevOps.Operations -MockWith {} + Mock -CommandName Get-R53VPCAssociationAuthorizationList -ModuleName Alkami.DevOps.Operations -MockWith { + $responseObject = @{ + VPCs = @( + @{ + VPCId = 'Test1' + VPCRegion = 'Test' + }, + @{ + VPCId = 'Test2' + VPCRegion = 'Test' + } + ) + } + return $responseObject + } + + Set-Route53HostedZoneVpcAssocations | Out-Null + + # Call count breakdown: + # - Get-AlkamiAwsProfileList: Called once at the beginning of the function. + # - Get-VpcIdList: Called once per Alkami profile per region -- two profiles, two regions. + # - Get-Route53HostedZoneIdList: Called once per profile. + # - Invoke-Parallel: Called once per profile per hosted zone -- two profiles, two hosted zones. + # - Get-R53VPCAssociationAuthorizationList: Called once per profile per hosted zone -- two profiles, two hosted zones. + # - Remove-R53VPCAssociationAuthorization: Called once per profile per hosted zone per authorization -- two profiles, two hosted zones, two authorizations. + Assert-MockCalled -CommandName Get-AlkamiAwsProfileList -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-VpcIdList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-Route53HostedZoneIdList -Times 2 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Invoke-Parallel -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Get-R53VPCAssociationAuthorizationList -Times 4 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + Assert-MockCalled -CommandName Remove-R53VPCAssociationAuthorization -Times 8 -Exactly -Scope It -ModuleName Alkami.DevOps.Operations + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Set-TracedMessagesEnabled.ps1 b/Modules/Alkami.DevOps.Operations/Public/Set-TracedMessagesEnabled.ps1 new file mode 100644 index 0000000..bcdb43c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Set-TracedMessagesEnabled.ps1 @@ -0,0 +1,125 @@ +function Set-TracedMessagesEnabled { + + <# +.SYNOPSIS + Sets TracedMessages Configuration for a Given Package to the Desired Value + +.DESCRIPTION + Sets TracedMessages Configuration for a Given Package to the Desired Value. This is only applicable for Alkami Chocolatey packages + +.PARAMETER DesiredLogLevel + The log level to set for the target namespaces + +.PARAMETER TargetNameSpaces + The namespaces which cover TracedMessages + +.Example + Set-TracedMessagesEnabled "Alkami.MicroServices.Superman2" -Enabled $false + +.Example + Set-TracedMessagesEnabled "Alkami.MicroServices.PredatoryPaydayLoans" -Enabled $true -TargetNameSpaces @("Alkami.App.Providers.AceCashExpress") +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 1)] + [ValidateSet("OFF","FATAL","ERROR","WARN","INFO","DEBUG", "TRACE")] + [string]$DesiredLogLevel, + + [Parameter(Mandatory = $false, Position = 2)] + [string[]]$TargetNameSpaces = @("Alkami.MicroServices.TracedMessages", "Alkami.ExternalServices.TracedMessages") + ) + + DynamicParam { + # Define the Paramater Attributes + $ParameterName = "PackageName" + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.HelpMessage = "The Package to target. Should be a directory in the Chocolatey lib folder" + $ParameterAttribute.Mandatory = $true + $ParameterAttribute.Position = 0 + $AttributeCollection.Add($ParameterAttribute) + + # Generate and add the ValidateSet + $parentPath = Join-Path -Path (Get-ChocolateyInstallPath) -ChildPath "lib" + $arrSet = Get-ChildItem -Path $parentPath -Directory -Depth 0 -ErrorAction SilentlyContinue | ForEach-Object {$_.Name} | Where-Object {$_.Length -gt 0} + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + + # Create the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary + } + + begin { + $PackageName = $PsBoundParameters[$ParameterName] + $logLead = Get-LogLeadName + } + + process { + + if ([String]::IsNullOrEmpty($PackageName)) { + + Write-Warning "$logLead : PackageName is null. Execution cannot continue" + return + } + + $libDirectory = Join-Path -Path (Get-ChocolateyInstallPath) -ChildPath "lib" + $targetDirectory = Join-Path -Path $libDirectory -ChildPath $PackageName + + if (!(Test-Path $targetDirectory)) { + + Write-Warning "$logLead : How did you manage this? The PackageName [$PackageName] doesn't exist!" + } + + Write-Host "$logLead : Looking for log4net config in $targetDirectory" + $logConfig = Get-ChildItem -Path $targetDirectory -File -Recurse -Depth 2 -Filter log4net.config + + # This is a little hacky but should work in all known cases + # The shortest path is the one we care about, SDK might have log4net in the SRC folder nested deeper + # While it should be more than 2 directories deep and so not found by the GCI above, who knows + # Not me + # Also works for App vs. Tools + if ($logConfig.Count -gt 1) { + + Write-Warning "$logLead : Found $($logConfig.Count) log4net.config files in the target folder. Will use the highest level one" + $targetLogConfig = $logConfig | Select-Object -ExpandProperty FullName | Sort-Object -Property Length | Select-Object -First 1 + Write-Host "$logLead : Will modify $targetLogConfig" + + } else { + + $targetLogConfig = $logConfig.FullName + } + + if ([String]::IsNullOrEmpty($targetLogConfig)) { + + Write-Warning "$logLead : Could not find a valid log config for package $PackageName. Check the folders and retry, or open a bug" + return + } + + $configXml = Read-XmlFile -xmlPath $targetLogConfig + $targetLoggers = $configXml.configuration.log4net.logger | Where-Object {$TargetNameSpaces -contains $_.Name} + + $saveRequired = $false + foreach ($appender in $targetLoggers) { + + $configuredLogLevel = $appender.level.value + if ($configuredLogLevel -ne $DesiredLogLevel) { + + Write-Host "$logLead : Updating $($appender.Name) from logLevel [$configuredLogLevel] to [$DesiredLogLevel]" + $appender.level.value = $DesiredLogLevel + $saveRequired = $true + } + } + + if ($saveRequired) { + + Save-XmlFile -xmlPath $targetLogConfig -xml $configXml + } else { + + Write-Host "$logLead : No modifications were made, no save required" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Show-NetTCPConnections.ps1 b/Modules/Alkami.DevOps.Operations/Public/Show-NetTCPConnections.ps1 new file mode 100644 index 0000000..5ab1ac2 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Show-NetTCPConnections.ps1 @@ -0,0 +1,53 @@ +function Show-NetTCPConnections { +<# +.SYNOPSIS +Displays Net TCP Connections with Process and User Information + +.DESCRIPTION +Shows Net TCP Connections with Process Name and User Name Decoration +Can display connections grouped by process ID or individually + +.PARAMETER ungroupConnections +[switch] Shows Ungrouped Connections + +.INPUTS +None + +.OUTPUTS +String[] + +.EXAMPLE +C:\PS> Show-NetTCPConnections +Connections Process Name Process ID UserName +----------- ------------ ---------- -------- + 147 Alkami.MicroServices.Broker.Host 9316 FH\pod12.micro$ + 41 Alkami.MicroServices.BillPayProviders.IPay.Service.Host 24280 FH\pod12.micro$ + +.EXAMPLE +C:\PS> Show-NetTCPConnections -ShowUngrouped +LocalPort LocalAddress RemotePort RemoteAddress State Process Name +--------- ------------ ---------- ------------- ----- ------------ + 80 0.0.0.0 0 0.0.0.0 Listen System + 80 :: 0 :: Listen System + 135 :: 0 :: Listen svchost + 135 0.0.0.0 0 0.0.0.0 Listen svchost +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [Alias("ShowUngrouped")] + [switch]$ungroupConnections + ) + + $connections = Get-DecoratedNetTCPConnections -ungroupConnections:$ungroupConnections + + if (!($ungroupConnections.IsPresent)) { + + $connections | Sort-Object -Property Count -Descending | Format-Table -Property @{Label="Connections";Expression={$_.Count}}, @{Label="Process Name";Expression={$_.ProcessName}}, @{Label="Process ID";Expression={$_.Name}}, UserName -AutoSize | Out-String + } + else { + + $connections | Sort-Object -Property LocalPort | Format-Table -Property LocalPort, LocalAddress, RemotePort, RemoteAddress, State, @{Label="Process Name";Expression={$_.ProcessName}}, UserName -AutoSize | Out-String + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.Tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.Tests.ps1 new file mode 100644 index 0000000..70b84d4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.Tests.ps1 @@ -0,0 +1,166 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsCurrentTimeInsideTenantTimeRange" { + + Context "Test Single Tenant" { + + # Return only one tenant just to validate the single-tenant case. + Mock -CommandName Get-FullTenantListFromServer -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = "FakeTenant1" + ConnectionString = "FakeConnectionString1" + } + ) + } + + # Return the current time zone. + Mock -CommandName Get-TenantTimeZone -ModuleName $moduleForMock -MockWith { + return "Greenwich Standard Time" + } + + $cases = @( + # Should return false before the time window. + @{ StartTime = 5; EndTime = 10; Time = 2; Expect = $false }, + + # Should return true during the time window. + @{ StartTime = 5; EndTime = 10; Time = 7; Expect = $true }, + + # Should return true at the start the time window. + @{ StartTime = 5; EndTime = 10; Time = 5; Expect = $true }, + + # Should return false at the end of the time window. + @{ StartTime = 5; EndTime = 10; Time = 10; Expect = $false }, + + # Should return false after the time window. + @{ StartTime = 5; EndTime = 10; Time = 13; Expect = $false } + + # Should work with overnight cases (If we want 11:00am->1:00am times, [23,1]) + @{ StartTime = 23; EndTime = 1; Time = 22; Expect = $false } + @{ StartTime = 23; EndTime = 1; Time = 23; Expect = $true } + @{ StartTime = 23; EndTime = 1; Time = 0; Expect = $true } + @{ StartTime = 23; EndTime = 1; Time = 1; Expect = $false } + + # Test that >=24 start times wrap around to 0 + @{ StartTime = 24; EndTime = 1; Time = 24; Expect = $true } + @{ StartTime = 25; EndTime = 2; Time = 25; Expect = $true } + @{ StartTime = 25; EndTime = 2; Time = 26; Expect = $false } + + # Test that <0 times wrap around to 24 + @{ StartTime = -1; EndTime = 1; Time = 0; Expect = $true } + + # Should return false with a time-window of 0 hours. + @{ StartTime = 5; EndTime = 5; Time = 14; Expect = $false } + ) + It "Should Return Expected Results" -TestCases $cases { + param($StartTime, $EndTime, $Time, $Expect) + + $now = (Get-Date) + $timeZoneInfo = [TimeZoneInfo]::FindSystemTimeZoneById("Greenwich Standard Time") + $testTime = [TimeZoneInfo]::ConvertTime($now, $timeZoneInfo) + $global:returnTime = $testTime.Date.AddHours($Time) + $global:localTimeZoneInfo = $timeZoneInfo + Mock -CommandName Get-Date -ModuleName $moduleForMock -MockWith { + return $global:returnTime + } + Mock -CommandName Get-TimeZone -ModuleName $moduleForMock -MockWith { + return $global:localTimeZoneInfo + } + + $arguments = @{ + MasterConnectionString = "FakeConnectionString" + StartTimeHour = $StartTime + EndTimeHour = $EndTime + } + (Test-IsCurrentTimeInsideTenantTimeRange @arguments) | Should -Be $Expect + } + } + + + Context "Test Multi Tenant" { + + # Return only one tenant just to validate the single-tenant case. + Mock -CommandName Get-FullTenantListFromServer -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = "FakeTenant1" + ConnectionString = "FakeConnectionString1" + }, + @{ + Name = "FakeTenant2" + ConnectionString = "FakeConnectionString2" + } + ) + } + + # Return the current time zone. + Mock -CommandName Get-TenantTimeZone -ModuleName $moduleForMock -MockWith { + param($TenantConnectionString) + + if($TenantConnectionString -eq "FakeConnectionString1") { + return "Greenwich Standard Time" + } else { + # Use a timezone +3 from GST + return "Arab Standard Time" + } + } + + $cases = @( + # NOTE: The second tenant timezone is +3 hours offset from GST. + + # Should return false before the time windows. + @{ StartTime = 5; EndTime = 10; Time = 2; Expect = $false }, + + # Should return false inside first tenant time window, but not the second tenant window. + # Lower threshold to enter the +3 tenant time window. + @{ StartTime = 5; EndTime = 10; Time = 1; Expect = $false }, + @{ StartTime = 5; EndTime = 10; Time = 2; Expect = $false }, + @{ StartTime = 5; EndTime = 10; Time = 3; Expect = $false }, + + # Should return true inside both tenant time windows. + # Lower threshold to enter both tenant time windows. + @{ StartTime = 5; EndTime = 10; Time = 4; Expect = $false } + @{ StartTime = 5; EndTime = 10; Time = 5; Expect = $true } + + # Should return false inside the second tenant's time window, but not the first tenant window. + # Upper threshold to exit the +3 time window. + @{ StartTime = 5; EndTime = 10; Time = 6; Expect = $true }, + @{ StartTime = 5; EndTime = 10; Time = 7; Expect = $false }, + @{ StartTime = 5; EndTime = 10; Time = 8; Expect = $false }, + + # Should return false after both tenant time windows. + # Threshold to exit both time windows. + @{ StartTime = 5; EndTime = 10; Time = 12; Expect = $false } + @{ StartTime = 5; EndTime = 10; Time = 13; Expect = $false } + @{ StartTime = 5; EndTime = 10; Time = 14; Expect = $false } + ) + It "Should Return Expected Results" -TestCases $cases { + param($StartTime, $EndTime, $Time, $Expect) + + $now = (Get-Date) + $timeZoneInfo = [TimeZoneInfo]::FindSystemTimeZoneById("Greenwich Standard Time") + $testTime = [TimeZoneInfo]::ConvertTime($now, $timeZoneInfo) + $global:returnTime = $testTime.Date.AddHours($Time) + $global:localTimeZoneInfo = $timeZoneInfo + Mock -CommandName Get-Date -ModuleName $moduleForMock -MockWith { + return $global:returnTime + } + Mock -CommandName Get-TimeZone -ModuleName $moduleForMock -MockWith { + return $global:localTimeZoneInfo + } + + $arguments = @{ + MasterConnectionString = "FakeConnectionString" + StartTimeHour = $StartTime + EndTimeHour = $EndTime + } + (Test-IsCurrentTimeInsideTenantTimeRange @arguments) | Should -Be $Expect + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.ps1 new file mode 100644 index 0000000..e6972ce --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-IsCurrentTimeInsideTenantTimeRange.ps1 @@ -0,0 +1,111 @@ +function Test-IsCurrentTimeInsideTenantTimeRange { + <# +.SYNOPSIS + This function returns true if the current-time is within an hourly time window for all tenants in a target pod in their local-times. + +.PARAMETER ComputerName + [string] A machine from a target environment to pull the master connection string from. + +.PARAMETER MasterConnectionString + [string] The master connection string of the pod. + +.PARAMETER StartTimeHour + [string] The inclusive start-hour of the time window that you want this function to return true within. (24 hour time) + +.PARAMETER EndTimeHour + [string] The exclusive end-hour of the time window that you want this function to return true within. (24 hour time) + +.NOTES + StartTimeHour is inclusive, and EndTimeHour is exclusive. For example [0,6] would match true from 12:00am to 5:59am in the tenant timezone: + + Test-IsCurrentTimeInsideTenantTimeRange -ComputerName "APP1234.some.domain" -StartTimeHour 0 -EndTimeHour 6 + #> + [CmdletBinding(DefaultParameterSetName = 'ByServer')] + param( + [Parameter(ParameterSetName = 'ByServer', Mandatory=$true)] + [string]$ComputerName, + [Parameter(ParameterSetName = 'ByConnectionString', Mandatory=$true)] + [string]$MasterConnectionString, + [Parameter(ParameterSetName = 'ByServer', Mandatory=$false)] + [Parameter(ParameterSetName = 'ByConnectionString', Mandatory=$false)] + [int]$StartTimeHour = 0, + [Parameter(ParameterSetName = 'ByServer', Mandatory=$false)] + [Parameter(ParameterSetName = 'ByConnectionString', Mandatory=$false)] + [int]$EndTimeHour = 6 + ) + + $logLead = (Get-LogLeadName) + + # Wrap negative times around to 24 hours, and >24 times back to 0. + if($StartTimeHour -lt 0) { + $StartTimeHour = 24 - (-$StartTimeHour % 24) + } elseif($StartTimeHour -gt 24) { + $StartTimeHour = $StartTimeHour % 24 + } + if($EndTimeHour -lt 0) { + $EndTimeHour = 24 - (-$EndTimeHour % 24) + } elseif($EndTimeHour -gt 24) { + $EndTimeHour = $EndTimeHour % 24 + } + + # Return false if the start is the same as the end hour. + if($StartTimeHour -eq $EndTimeHour) { + return $false + } + + # If the user has provided a server to pull a connection string from, do so. + if($PSCmdlet.ParameterSetName -eq "ByServer") { + $MasterConnectionString = Get-ConnectionString -name "AlkamiMaster" -ComputerName $ComputerName + } + + # Make sure the connection string is good. + if([string]::IsNullOrWhiteSpace($MasterConnectionString)) { + Write-Error "$logLead : Master Connection String cannot be null or whitespace." + } + + # Pull tenants (and their connection strings) from the master connection string. + $tenants = Get-FullTenantListFromServer -connectionString $MasterConnectionString + + # Iterate over each tenant and figure out if we the current time is inside the local-time time window for each FI. + $insideAllTimeRanges = $true + foreach($tenant in $tenants) { + + # Query for the timezone of the FI out of the database. + try { + $timeZoneName = Get-TenantTimeZone -TenantConnectionString $tenant.ConnectionString + } catch { + throw "$logLead : Could not query time zone from tenant $($tenant.Name)" + } + + # Convert from the local time to the FI's time zone. + # Don't use UTC because we want the time to convert to the tenant's local time. + $timeZoneInfo = [TimeZoneInfo]::FindSystemTimeZoneById($timeZoneName) + $currentTimeZone = (Get-TimeZone) + $currentTime = (Get-Date) + $tenantTime = [TimeZoneInfo]::ConvertTime($currentTime, $currentTimeZone, $timeZoneInfo) + + Write-Verbose "Tenant: $($tenant.Name)`n Current Time: $currentTime`n Tenant Time: $tenantTime`n Tenant Time Zone: $timeZoneName" + + # If startTime > endTime + # startTime = -(24 - startTime) + # endTime = endTime + + # Figure out if we are inside the time range for this particular FI. + $fiHour = $tenantTime.Hour + + # Figure out if we are inside the time range. + # Account for if the start time is larger than the end time, ie if someone wants 11:00pm-1:00am at [23,1] + if($StartTimeHour -lt $EndTimeHour) { + $insideTimeRange = ($fiHour -ge $StartTimeHour) -and ($fiHour -lt $EndTimeHour) + } + if($StartTimeHour -gt $EndTimeHour) { + $insideTimeRange = ($fiHour -ge $StartTimeHour) -or ($fiHour -lt $EndTimeHour) + } + if(!$insideTimeRange) { + $insideAllTimeRanges = $false + Write-Verbose "$logLead : Not inside the expected time range of [$StartTimeHour-$EndTimeHour] for tenant `"$($tenant.Name)`"" + } + } + + return $insideAllTimeRanges +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-OpenPorts.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-OpenPorts.ps1 new file mode 100644 index 0000000..02aee0d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-OpenPorts.ps1 @@ -0,0 +1,215 @@ +function Test-OpenPorts { +<# +.SYNOPSIS + Tests common ports to see if they're open. Requires the target services to be running on the other side. +.PARAMETER vipPrefix + +Alias: VIPSubnet +The first 3 segments of the VIP Subnet. For example, for POD 8.3, the parameter value should be 10.100.83 +.PARAMETER machines + +Alias: MachineList +A comma separated list of web and app servers to test. Useful when the broadcasters value is not set up. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("VIPSubnet")] + [string]$vipPrefix, + + [Parameter(Mandatory=$false)] + [Alias("MachineList")] + [string]$machines + ) + + $logLead = (Get-LogLeadName); + + $subscriptionServicePort = 50001 # Only server(s) running the subscription service will be listening on this port + + $appServerPorts = ( + $subscriptionServicePort, + 12016, + 8001, + 808, + 80 + ) + + if (!([string]::IsNullOrEmpty($vipPrefix))) + { + [string[]]$vips = @( + $vipPrefix.TrimEnd('.') + ".50" + $vipPrefix.TrimEnd('.') + ".51" + $vipPrefix.TrimEnd('.') + ".52" + $vipPrefix.TrimEnd('.') + ".53" + $vipPrefix.TrimEnd('.') + ".54" + $vipPrefix.TrimEnd('.') + ".55" + $vipPrefix.TrimEnd('.') + ".56" + $vipPrefix.TrimEnd('.') + ".57" + $vipPrefix.TrimEnd('.') + ".58" + $vipPrefix.TrimEnd('.') + ".59" + $vipPrefix.TrimEnd('.') + ".60" + $vipPrefix.TrimEnd('.') + ".61" + $vipPrefix.TrimEnd('.') + ".62" + ) + } + + # Get the first client from the master for DB checks + if (Test-IsAppServer) { + [array]$clients += Get-CatalogsFromMaster + $client = $clients[0] + } + + # Load the AD Module (and install if needed) + Install-ActiveDirectoryModule -Verbose:$false | Out-Null + + # Figure out which database server to check + if (Test-IsAppServer) { + $masterConnectionString = Get-MasterConnectionString + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + $dbServer = $conStrBuilder.DataSource + } else { + $reportServer = (Get-AppSetting "ReportServer") + [System.Uri]$reportServerUri = $null + if (($null -eq $reportServer) -or + !( + [System.URI]::TryCreate($reportServer, [System.UriKind]::Absolute, [ref]$reportServerUri) -and + ($reportServerUri.Scheme -eq [System.Uri]::UriSchemeHttp -or $reportServerUri.Scheme -eq [System.Uri]::UriSchemeHttps) + ) + ) { + Write-Verbose ("$logLead : Unable to Create URI from ReportServer appSetting") + $dbServer = $null + } else { + $dbServer = $reportServerUri.Host + } + } + + Write-Verbose ("$logLead : DBServer Read as {0}" -f $dbServer) + + if ([String]::IsNullOrEmpty($machines)) + { + + #TODO use alk Tags to get machine list + # Figure out all servers in the POD from the the servers in this server's OU + # This won't work at AWS and will only work if the OU contains all the servers for the POD + # It's also pretty hacky + $currentOU = Get-ComputerOU + Write-Verbose ("$logLead : Pulling computers from OU {0} for web/app server checks" -f $currentOU) + $broadCasters = (Get-ADComputer -Filter * -SearchBase $currentOU) + + # Try to get the Broadcasters Values first + $bcValue = Get-AppSetting "Broadcasters" -ErrorAction SilentlyContinue + + if ($null -ne $bcValue) { + Write-Verbose ("$logLead : Using Broadcasters value from machine.config for web/app server checks") + + $webServers = $bcValue.Split(",") | Where-Object {$_ -match "VMW"} + $appServers = $bcValue.Split(",") | Where-Object {$_ -match "VMA"} + + Write-Verbose ("$logLead : Web Servers To Test: {0}" -f ($webServers -join ", ")) + Write-Verbose ("$logLead : App Servers To Test: {0}" -f ($appServers -join ", ")) + } elseif ($null -ne $dcValue) { + Write-Verbose ("$logLead : Using Broadcasters value from app.config for web/app server checks") + + $webServers = $dcValue.Split(",") | Where-Object {$_ -match "VMW"} + $appServers = $dcValue.Split(",") | Where-Object {$_ -match "VMA"} + + Write-Verbose ("$logLead : Web Servers To Test: {0}" -f ($webServers -join ", ")) + Write-Verbose ("$logLead : App Servers To Test: {0}" -f ($appServers -join ", ")) + } else { + # Figure out all servers in the POD from the the servers in this server's OU + # This won't work at AWS and will only work if the OU contains all the servers for the POD + # It's also pretty hacky + $currentOU = Get-ComputerOU + Write-Verbose ("$logLead : Pulling computers from OU {0} for web/app server checks" -f $currentOU) + $broadCasters = (Get-ADComputer -Filter * -SearchBase $currentOU) + + $webServers = $broadCasters | Where-Object {$_.Name -match "VMW"} + $appServers = $broadCasters | Where-Object {$_.Name -match "VMA"} + + Write-Verbose ("$logLead : Web Servers To Test: {0}" -f ($webServers.Name -join ", ")) + Write-Verbose ("$logLead : App Servers To Test: {0}" -f ($appServers.Name -join ", ")) + } + } else { + [array]$machineArray = $machines.Split(",") | ForEach-Object {$_.Trim()} + Write-Verbose ("$logLead : Parsed {0} machines from parameter" -f $machineArray.Count) + + # This won't work at AWS + [string[]]$webServers = $machineArray | Where-Object {$_ -match "VMW"} + [string[]]$appServers = $machineArray | Where-Object {$_ -match "VMA"} + + Write-Verbose ("$logLead : Web Servers To Test: {0}" -f ($webServers -join ", ")) + Write-Verbose ("$logLead : App Servers To Test: {0}" -f ($appServers -join ", ")) + } + + # Get the Entrust URL if We Need to Test It + if (Test-IsAppServer) { + Write-Verbose ("$logLead : Getting Entrust URL from {0}" -f $client.ConnectionString) + $entrustUrlFromDatabase = Get-EntrustAdminUrlFromClient $client + [System.Uri]$entrustUri = $null + if (!([System.URI]::TryCreate($entrustUrlFromDatabase, [System.UriKind]::Absolute, [ref]$entrustUri) -and ($entrustUri.Scheme -eq [System.Uri]::UriSchemeHttp -or $entrustUri.Scheme -eq [System.Uri]::UriSchemeHttps))) { + Write-Warning ("$logLead : Unable to Read Entrust Server from Client Database") + $entrustServer = $null + } else { + $entrustServer = $entrustUri.Host + } + } else { + $entrustServer = $null + } + + Write-Verbose ("$logLead : Entrust Server To Test: {0}" -f $entrustServer) + + # Get Redis Endpoints to Test + $rawRedisConnectionString = Get-RedisConnectionString $client + $redisConnectionString = $rawRedisConnectionString.Split(":") | Select-Object -First 1 + $redisConnectionPort = $rawRedisConnectionString.Split(":") | Select-Object -Last 1 | ForEach-Object {$_.Split(",")} | Select-Object -First 1 + + Write-Verbose ("$logLead : RedisConnectionString to Test: {0}:{1}" -f $redisConnectionString, $redisConnectionPort) + + [string[]]$redisIPs = ([System.Net.DNS]::GetHostEntry($redisConnectionString).AddressList | Select-Object -ExpandProperty IPAddressToString) + Write-Verbose ("$logLead : Redis IPs to Test: {0}" -f ($redisIPs -join ", ")) + + $domainControllers = (Get-ADGroupMember 'Domain Controllers' | Select-Object -ExpandProperty name) + Write-Verbose ("$logLead : Domain Controllers to Test: {0}" -f ($domainControllers -join ", ")) + + if (!([String]::IsNullOrEmpty($vips))) + { + Write-Verbose ("$logLead : VIPs to Test: {0}" -f ($vips -join ", ")) + } + + # Get WSUS Endpoints to Test + $windowsUpdatePolicy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' + $useWsusKey = Get-ItemProperty ($windowsUpdatePolicy + "\AU") -ErrorAction SilentlyContinue | Select-Object -ExpandProperty UseWUServer -ErrorAction SilentlyContinue + + if ($null -ne $useWsusKey -and $useWsusKey -eq 1) + { + [System.Uri]$uri = Get-ItemProperty $windowsUpdatePolicy | Select-Object -ExpandProperty WUServer + Write-Verbose ("$logLead : WSUS Servers to Test: {0}" -f $uri.Host) + } + else + { + Write-Verbose ("$logLead : Server is Not Configured to use WSUS") + } + + $portList = @() + + foreach ($server in $appServers) + { + if ($null -ne $server.Name) + { + $serverName = $server.Name + } + else + { + $serverName = $server + } + + foreach ($appServerPort in $appServerPorts) + { + Write-Verbose ("$logLead : Adding Type:App, Server:{0}, Port={1}" -f $serverName, $appServerPort) + $portList += @{Type = "ORB App"; Server = $serverName; Port = $appServerPort; } + } + + # This doesn't work in Armor for reasons unknown... + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-SmtpServer.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-SmtpServer.ps1 new file mode 100644 index 0000000..7cbacb0 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-SmtpServer.ps1 @@ -0,0 +1,146 @@ +function Test-SmtpServer { + <# +.SYNOPSIS + Test if email is correctly set up for a given FI + +.PARAMETER FIname + Name of the FI to test + +.PARAMETER SendTo + Intended recipient of the test email + +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FIname, + [Parameter(Mandatory = $false)] + [string]$SendTo + ) + + $logLead = Get-LogLeadName + + try { + Import-Module ActiveDirectory + } catch { + Write-Error "$logLead : Error loading AD module" + } + + # Get the current AD User's info including the extended mail property + $currentUser = Get-ADUser $env:username -Server "corp.alkamitech.com" -Properties mail + + # If $SendTo was not provided as an argument and we can't pull the AD email, then respond with error + if ([string]::IsNullOrEmpty($currentUser.mail) -and [string]::IsNullOrEmpty($SendTo)) { + Write-Error "$logLead : Unable to dynamically pull AD email. Call this function against and provide a SendTo argument value with an email" + return + } elseif ([string]::IsNullOrEmpty($SendTo)) { + # Set the $SendTo value as the AD User's email address + $SendTo = $currentUser.mail + } + + # Read connectionstring from machine.config for master + try { + $MasterConnectionString = Get-ConnectionString -name "AlkamiMaster" + } catch { + Write-Error "$logLead : Could not get/find a Master Connection String in machine.config - $($_.Exception.Message)" + return + } + + # Open connection to AlkamiMaster to get FI DB ConnectionString + $Connection = New-Object System.Data.SqlClient.SqlConnection + $Connection.ConnectionString = $MasterConnectionString + + $Command = New-Object System.Data.SQLClient.SQLCommand + $Command.Connection = $Connection + $Command.CommandText = "select connectionstring as ConnectionString + from dbo.tenant + where bankurlsignatures like '%$FIName%'" + $errorDuringQuery = "" + + # Check Master dbo.Tenant to find FI + try { + $Connection.Open() + $FIDBConnectionString = $Command.ExecuteScalar() + + if ([string]::IsNullOrEmpty($FIDBConnectionString)) { #no match in DB, we are done + $errorDuringQuery = "$logLead : No match in AlkamiMaster for $FIname." + } + } catch { + $errorDuringQuery = "$logLead : Error: $($_.Exception.Message)" + } finally { + $Connection.Close() + if (![string]::IsNullOrEmpty($errorDuringQuery)) { + Write-Error $errorDuringQuery + } + } + + + $Command.CommandText = "select its.Name as KeyName, its.Value as KeyValue + from core.itemsetting as its + join core.item as i + on i.id = its.itemid + where i.name like '%smtp%' + and its.name in ('Sender Address','SMTP Password','SMTP Port','SMTP Server','SMTP Username','Use SSL');" + + $Connection.ConnectionString = $FIDBConnectionString + + + try { + $Connection.Open() + $reader = $Command.ExecuteReader() + Write-Host $Connection.ConnectionString + + # Verify that there was a return result + if (!$reader.HasRows) { + Write-Warning "No matches for SMTP key/values in $Database for $FIname. Script ending..." + return + } + + while ( $reader.Read()) { + switch ($reader[0]) { + "SMTP Port" {$SMTPPort = $reader[1];break} + + "Sender Address" {$testFrom = $reader[1];break} + + "SMTP Password" {$SMTPPassword = ConvertTo-SecureString $reader[1] -AsPlainText -Force;break} + + "SMTP Server" {$SMTPServer = $reader[1];break} + + "SMTP Username" {$SMTPUsername = $reader[1];break} + + "Use SSL" {$UseSSL = $reader[1];break} + default {break} + } + } + $reader.Close() + + } catch { + $errorDuringQuery = "$logLead : Error: $($_.Exception.Message)" + } finally { + $Connection.Close() + if (![string]::IsNullOrEmpty($errorDuringQuery)) { + Write-Error $errorDuringQuery + } + } + + $testSubject = "Test email from $FIname" + $testBody = "Test Body from $FIname" + + $SMTPMessage = New-Object System.Net.Mail.MailMessage($testFrom,$SendTo,$testSubject,$testBody) + + $SMTPClient = New-Object Net.Mail.SmtpClient($SmtpServer, $SMTPPort) + $SMTPClient.EnableSsl = $UseSSL + $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($SMTPUsername, $SMTPPassword); + + try { + $SMTPClient.Send($SMTPMessage) + } catch { + $errorDuringQuery = "$logLead : Error: $($_.Exception.Message)" + } finally { + if (![string]::IsNullOrEmpty($errorDuringQuery)) { + Write-Error $errorDuringQuery + } else { + Write-Host "$logLead : Email successfully sent to $SendTo" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.ps1 new file mode 100644 index 0000000..7a14699 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.ps1 @@ -0,0 +1,95 @@ +function Test-SymConnectNonLoopbackExists { + <# + .SYNOPSIS + Tests hosts file for a non loopback entry for SymconnectMultiplexer + .DESCRIPTION + Uses a regex to look for non loopback entry, returns true is non loopback found, returns false if not found. + .PARAMETER ComputerName + The computer name of the remote machine. + .EXAMPLE + $TestSymConnectNonLoopbackExists = Test-SymConnectNonLoopbackExists + .INPUTS + none + .OUTPUTS + $bool + .NOTES + Explanation of the regex + ^ asserts position at start of a line + \s matches any whitespace character + * matches the previous token between zero and unlimited times, as many times as possible, giving back as needed (greedy) + Negative Lookahead (?!127\.0{1,3}\.0{1,3}\.0{0,2}1) + Assert that the Regex below does not match + 127 matches the characters 127 literally (case sensitive) + \. matches the character . literally (case sensitive) + 0 matches the character 0 literally (case sensitive) + {1,3} matches the previous token between 1 and 3 times, as many times as possible, giving back as needed (greedy) + \. matches the character . literally (case sensitive) + 0 matches the character 0 literally (case sensitive) + {1,3} matches the previous token between 1 and 3 times, as many times as possible, giving back as needed (greedy) + \. matches the character . literally (case sensitive) + 0 matches the character 0 literally (case sensitive) + 1 matches the character 1 literally (case sensitive) + 1st Capturing Group ((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} + {3} matches the previous token exactly 3 times + A repeated capturing group will only capture the last iteration. Put a capturing group around the repeated group to capture all iterations or use a non-capturing group instead if you're not interested in the data + 2nd Capturing Group (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) + 1st Alternative 25[0-5] + 25 matches the characters 25 literally (case sensitive) + Match a single character present in the list below [0-5] + 2nd Alternative 2[0-4][0-9] + 2 matches the character 2 literally (case sensitive) + Match a single character present in the list below [0-4] + Match a single character present in the list below [0-9] + 3rd Alternative [01]?[0-9][0-9]? + \. matches the character . literally (case sensitive) + 3rd Capturing Group (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) + 1st Alternative 25[0-5] + 25 matches the characters 25 literally (case sensitive) + Match a single character present in the list below [0-5] + 0-5 matches a single character in the range between 0 (index 48) and 5 (index 53) (case sensitive) + 2nd Alternative 2[0-4][0-9] + 2 matches the character 2 literally (case sensitive) + Match a single character present in the list below [0-4] + 0-4 matches a single character in the range between 0 (index 48) and 4 (index 52) (case sensitive) + Match a single character present in the list below [0-9] + 3rd Alternative [01]?[0-9][0-9]? + Match a single character present in the list below [01] + ? matches the previous token between zero and one times, as many times as possible, giving back as needed (greedy) + 01 matches a single character in the list 01 (case sensitive) + Match a single character present in the list below [0-9] + Match a single character present in the list below [0-9] + \s matches any whitespace character (equivalent to [\r\n\t\f\v ]) + + matches the previous token between one and unlimited times, as many times as possible, giving back as needed (greedy) + SymConnectMultiplexer matches the characters SymConnectMultiplexer literally (case sensitive) + . matches any character (except for line terminators) + * matches the previous token between zero and unlimited times, as many times as possible, giving back as needed (greedy) + $ asserts position at the end of a line + + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName = $null + ) + + $logLead = (Get-LogLeadName) + + if (Test-StringIsNullOrWhitespace -Value $ComputerName) { + $ComputerName = $env:COMPUTERNAME + } + + $hostsFilePath = Get-UncPath -FilePath "$env:windir\System32\Drivers\etc\hosts" -ComputerName $ComputerName -IgnoreLocalPaths + $hostContent = Get-HostsFileContent -hostsPath $hostsFilePath + Write-Host "$logLead : Checking $ComputerName host file for SymconnectMultiplexer loopback." + $regex2 = '^\s*(?!127\.0{1,3}\.0{1,3}\.0{0,2}1)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s+SymConnectMultiplexer.*$' + + $selectedHostLine = $hostContent | Select-String -Pattern $regex2 + if ($selectedHostLine.count -gt 0) { + Write-Host "$logLead : Found the following non loopback entries on host $env:COMPUTERNAME:" + Write-Host $selectedHostLine + return $true + } else { + Write-Host "$loglead : No non loopback entries found for SymconnectMultiplexer" + return $false + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.tests.ps1 new file mode 100644 index 0000000..a96b9f7 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-SymConnectNonLoopbackExists.tests.ps1 @@ -0,0 +1,75 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-SymConnectNonLoopbackExists" { + + Mock Write-Host {} + + Context "Test single line Regex accuracy" { + It "returns true for non loopback with comment" { + Mock -CommandName Get-HostsFileContent -MockWith { + return "123.4.5.6 SymConnectMultiplexer #this can be set back to 127.0.0.1 later" + } + Test-SymConnectNonLoopbackExists | Should -BeTrue + } + It "returns true for non loopback" { + Mock -CommandName Get-HostsFileContent -MockWith { + return "123.4.5.6 SymConnectMultiplexer" + } + Test-SymConnectNonLoopbackExists | Should -BeTrue + } + It "returns false for non loopback with not symconnect hostname" { + Mock -CommandName Get-HostsFileContent -MockWith { + return "123.4.5.6 bankservice" + } + Test-SymConnectNonLoopbackExists | Should -beFalse + } + It "returns false for loopback and not symmconnect hostname " { + Mock -CommandName Get-HostsFileContent -MockWith { + return "127.0.0.1 bankservice" + } + Test-SymConnectNonLoopbackExists | Should -BeFalse + } + It "returns false for loopback with spaces " { + Mock -CommandName Get-HostsFileContent -MockWith { + return " 127.0.0.1 bankservice " + } + Test-SymConnectNonLoopbackExists | Should -BeFalse + } + It "returns false for commented line" { + Mock -CommandName Get-HostsFileContent -MockWith { + return "# 127.0.0.1 SymConnectMultiplexer" + } + Test-SymConnectNonLoopbackExists | Should -BeFalse + } + + } + Context "Test multi line Regex accuracy" { + It "returns true for non loopback with comment and commented loopback" { + Mock -CommandName Get-HostsFileContent -MockWith { + return @("123.4.5.6 SymConnectMultiplexer #this can be set back to 127.0.0.1 later", + "# 127.0.0.1 SymConnectMultiplexer") + } + Test-SymConnectNonLoopbackExists | Should -BeTrue + } + It "returns true for non loopback and commented non loopback" { + Mock -CommandName Get-HostsFileContent -MockWith { + return @("123.4.5.6 SymConnectMultiplexer", + "# 123.4.5.6 SymConnectMultiplexer #this can be set back to 127.0.0.1 later") + } + Test-SymConnectNonLoopbackExists | Should -BeTrue + } + It "returns false for non loopback with not symconnect hostname and commented loopback" { + Mock -CommandName Get-HostsFileContent -MockWith { + return @("123.4.5.6 bankservice", + "#127.0.0.1 SymConnectMultiplexer") + } + Test-SymConnectNonLoopbackExists | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.ps1 new file mode 100644 index 0000000..4100c08 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.ps1 @@ -0,0 +1,94 @@ +function Test-SymitarPorts { +<# + +.SYNOPSIS + Test a Symitar IP address and list of ports for connectivity. + +.DESCRIPTION + Loops over the input list of ports and calls 'Test-TcpConnection' for each port. + +.PARAMETER ipAddress + [string] The IP address of the Symitar host to test. + +.PARAMETER ports + [uint32[]] The array of ports on the Symitar host to test. + +.PARAMETER msTimeout + [uint32] Optional - the connection timeout in milliseconds. Default is 500. + +.PARAMETER msDelay + [uint32] Optional - the delay between each port test in milliseconds. Default is 0. + +.EXAMPLE + Test-SymitarPorts -ipAddress '10.0.28.11' -ports @(12138, 12139) + + Port Result + ---- ------ +12138 True +12139 True + + +True + +.EXAMPLE + Test-SymitarPorts -ipAddress '10.0.28.11' -ports (12138..12140) + + Port Result + ---- ------ +12138 True +12139 False +12140 True + + +False +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [string]$ipAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [uint32[]]$ports, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, [uint32]::MaxValue)] + [uint32]$msTimeout = 500, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, [uint32]::MaxValue)] + [uint32]$msDelay = 0 + ) + + $isApp = Test-IsAppServer + $isMic = Test-IsMicroServer + + if ( ( $isApp -eq $false ) -and ( $isMic -eq $false ) ) { + + Write-Error "This function is only available on an App or Mic VM." + return $null + } + + $output = @() + $finalResult = $true + + foreach ( $port in $ports ) { + + $result = (Test-TcpConnection -ipAddress $ipAddress -port $port -msTimeout $msTimeout -requestMessage "`n" -expectedResponse "RSWHAT?`n") + + $finalResult = ( $finalResult -and $result ) + + $output += New-Object PSObject -Property @{ + 'Port' = $port + 'Result' = $result + } + + Start-Sleep -Milliseconds $msDelay + } + + $output | Format-Table -AutoSize -Property Port, Result | Out-String + + return $finalResult +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.tests.ps1 new file mode 100644 index 0000000..89f8a4c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-SymitarPorts.tests.ps1 @@ -0,0 +1,82 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-SymitarPorts" { + + Context "Input Validation" { + + It "IP Address Should Not Be Null" { + + { Test-SymitarPorts -ipAddress $null } | Should Throw + } + + It "IP Address Should Not Be Empty" { + + { Test-SymitarPorts -ipAddress '' } | Should Throw + } + + It "Ports Array Should Not Be Null" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports $null } | Should Throw + } + + It "Ports Array Should Not Be Empty" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @() } | Should Throw + } + + It "Ports Array Should Contain Positive Integers" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(-1) } | Should Throw + } + + It "Ports Array Should Contain Unsigned 32-bit Integers" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @([uint32]::MaxValue + 1) } | Should Throw + } + + It "Timeout Should Be Natural Number" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(12345) -msTimeout -1 } | Should Throw + } + + It "Timeout Should Be Unsigned 32-bit Integer" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(12345) -msTimeout ([uint32]::MaxValue + 1) } | Should Throw + } + + It "Delay Should Be Positive Integer" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(12345) -msDelay -1 } | Should Throw + } + + It "Delay Should Be Unsigned 32-bit Integer" { + + { Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(12345) -msDelay ([uint32]::MaxValue + 1) } | Should Throw + } + } + + Context "Tier Validation" { + + It "Should Be Run on App Or Microservice Machine" { + + Mock -CommandName Test-IsAppServer -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Test-IsMicroServer -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + $result = ( Test-SymitarPorts -ipAddress '127.0.0.1' -ports @(12345) ) + + Assert-MockCalled -CommandName Test-IsAppServer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Test-IsMicroServer -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "This function is only available on an App or Mic VM." } + + $result | Should -BeFalse + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.ps1 new file mode 100644 index 0000000..3aaa70d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.ps1 @@ -0,0 +1,109 @@ +function Test-TcpConnection { +<# + +.SYNOPSIS + Test an IP address and port for TCP connectivity. + +.DESCRIPTION + Establishes a TCP connection to the specified IP address and port. + Optionally, sends a string request to the connection and compares + the response to a specified expected value. + +.PARAMETER ipAddress + [string] The IP address of the host to test. + +.PARAMETER port + [uint32] The port on the host to test. + +.PARAMETER msTimeout + [uint32] The connection timeout in milliseconds. + +.PARAMETER requestMessage + [string] The request message to send on the TCP connection. + +.PARAMETER expectedResponse + [string] The expected response message from the TCP connection. + +.EXAMPLE + Test-TcpConnection -ipAddress '10.0.28.11' -port 12138 -msTimeout 5000 + +True + +.EXAMPLE + Test-TcpConnection -ipAddress '10.0.28.11' -port 12138 -msTimeout 5000 -requestMessage "`n" -expectedResponse "RSWHAT?`n" + +True +#> + + [CmdletBinding(DefaultParameterSetName='Default')] + param( + [Parameter(ParameterSetName='Default', Mandatory = $true)] + [Parameter(ParameterSetName='ReqRes' , Mandatory = $true)] + [ValidateNotNullorEmpty()] + [string]$ipAddress, + + [Parameter(ParameterSetName='Default', Mandatory = $true)] + [Parameter(ParameterSetName='ReqRes' , Mandatory = $true)] + [ValidateRange(1, [uint32]::MaxValue)] + [uint32]$port, + + [Parameter(ParameterSetName='Default', Mandatory = $true)] + [Parameter(ParameterSetName='ReqRes' , Mandatory = $true)] + [ValidateRange(1, [uint32]::MaxValue)] + [uint32]$msTimeout, + + [Parameter(ParameterSetName='ReqRes', Mandatory = $true)] + [ValidateNotNullorEmpty()] + [string]$requestMessage, + + [Parameter(ParameterSetName='ReqRes', Mandatory = $true)] + [ValidateLength(1,4096)] + [string]$expectedResponse + ) + + $tcpStream = $null + $tcpObject = $null + $tcpConnect = $null + $result = $false + + try { + + # Attempt the connection. + $tcpObject = New-Object System.Net.Sockets.TcpClient + $tcpConnect = $tcpObject.BeginConnect( $ipAddress, $port, $null, $null ) + $result = $tcpConnect.AsyncWaitHandle.WaitOne( $msTimeout , $false ) + + if ( $result -and $PSBoundParameters.ContainsKey('requestMessage') ) { + + # Write the request message, converting the input to an ASCII byte array. + $tcpStream = $tcpObject.GetStream() + $message = [System.Text.Encoding]::ASCII.GetBytes($requestMessage) + $tcpStream.Write( $message, 0, $message.Length ) + + # Read the response, converting the ASCII byte array back to a string + # (for ease of debugging). + $buf = New-Object System.Byte[] 4096 + $count = $tcpStream.Read( $buf, 0, 4096 ) + $response = [System.Text.Encoding]::ASCII.GetString( $buf, 0, $count ) + + $result = ( $expectedResponse -eq $response ) + } + + } catch { + + $_.Exception | Format-List -force + $result = $false + + } finally { + + if ( $null -ne $tcpStream ) { + $tcpStream.Close() + } + + if ( $null -ne $tcpObject ) { + $tcpObject.Close() + } + } + + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.tests.ps1 new file mode 100644 index 0000000..61e432e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-TcpConnection.tests.ps1 @@ -0,0 +1,71 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-TcpConnection" { + + Context "Input Validation" { + + It "IP Address Should Not Be Null" { + + { Test-TcpConnection -ipAddress $null } | Should Throw + } + + It "IP Address Should Not Be Empty" { + + { Test-TcpConnection -ipAddress '' } | Should Throw + } + + It "Port Should Be Positive Integer" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port -1 } | Should Throw + } + + It "Port Should Be Unsigned 32-bit Integer" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port ([uint32]::MaxValue + 1) } | Should Throw + } + + It "Timeout Should Be Natural Number" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout -1 } | Should Throw + } + + It "Timeout Should Be Unsigned 32-bit Integer" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout ([uint32]::MaxValue + 1) } | Should Throw + } + + It "Request Message Should Not Be Null" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout 1 -requestMessage $null } | Should Throw + } + + It "Request Message Should Not Be Empty" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout 1 -requestMessage '' } | Should Throw + } + + It "Expected Response Should Not Be $null" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout 1 -requestMessage 'Test' -expectedResponse $null } | Should Throw + } + + It "Expected Response Should Not Be Empty" { + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout 1 -requestMessage 'Test' -expectedResponse '' } | Should Throw + } + + It "Expected Response Should Not Be Greater Than 4096 Bytes" { + + $buffer = [System.Byte[]] (,0x65 * 4097) + $bufferStr = [System.Text.Encoding]::ASCII.GetString( $buffer, 0, 4097 ) + + { Test-TcpConnection -ipAddress '127.0.0.1' -port 12345 -msTimeout 1 -requestMessage 'Test' -expectedResponse $bufferStr } | Should Throw + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.ps1 new file mode 100644 index 0000000..bc2ff12 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.ps1 @@ -0,0 +1,58 @@ +function Test-VpcExists { + +<# +.SYNOPSIS + Determine if a VPC exists using the specified parameters. + +.PARAMETER Id + [string] The ID of the VPC. + +.PARAMETER Region + [string] The AWS region of the VPC (e.g. 'us-east-1'). + +.PARAMETER ProfileName + [string] The AWS profile where the VPC is located (e.g. 'temp-prod'). + +.OUTPUTS + [Boolean] True if the VPC exists; false otherwise. + +.EXAMPLE + Test-VpcExists -Id 'vpc-1234' -Region 'us-east-1' -ProfileName 'temp-prod' + +True +#> + + [OutputType([Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Id, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName + ) + + $result = $false + $logLead = (Get-LogLeadName) + + Import-AWSModule + + try { + + # Attempt the VPC lookup using the arguments provided. + $vpcEntry = ( Get-EC2Vpc -VpcId $Id -ProfileName $ProfileName -Region $Region ) + $result = ( $null -ne $vpcEntry ) + + } catch { + + Write-Verbose "$logLead : $_" + } + + return $result +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.tests.ps1 new file mode 100644 index 0000000..2581d02 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Test-VpcExists.tests.ps1 @@ -0,0 +1,91 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-VpcExists" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { + return @( + @{ 'Region' = 'us-east-1' }, + @{ 'Region' = 'us-west-2' } + ) + } + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Test-VpcExists.tests' } + + Context "Input Validation" { + + It "ID Should Not Be Null" { + + { Test-VpcExists -Id $null } | Should -Throw + } + + It "ID Should Not Be Empty" { + + { Test-VpcExists -Id '' } | Should -Throw + } + + It "Region Should Not Be Null" { + + { Test-VpcExists -Id 'Test' -Region $null } | Should -Throw + } + + It "Region Should Not Be Empty" { + + { Test-VpcExists -Id 'Test' -Region '' } | Should -Throw + } + + It "Region Should Be In Supported Regions List" { + + { Test-VpcExists -Id 'Test' -Region 'Test' } | Should -Throw + } + + It "ProfileName Should Not Be Null" { + + { Test-VpcExists -Id 'Test' -Region 'us-east-1' -ProfileName $null } | Should -Throw + } + + It "ProfileName Should Not Be Empty" { + + { Test-VpcExists -Id 'Test' -Region 'us-east-1' -ProfileName '' } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Return False When AWS Call Throws" { + + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { throw "This is a test." } + + $result = Test-VpcExists -Id 'Test' -Region 'us-east-1' -ProfileName 'Test' + $result | Should -BeFalse + + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Return False When VPC Not Found" { + + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return $null } + + $result = Test-VpcExists -Id 'Test' -Region 'us-east-1' -ProfileName 'Test' + $result | Should -BeFalse + + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Return True When VPC Found" { + + Mock -CommandName Get-EC2Vpc -ModuleName $moduleForMock -MockWith { return 'Test' } + + $result = Test-VpcExists -Id 'Test' -Region 'us-east-1' -ProfileName 'Test' + $result | Should -BeTrue + + Assert-MockCalled -CommandName Get-EC2Vpc -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-AlkamiDatabase.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-AlkamiDatabase.ps1 new file mode 100644 index 0000000..8111fd9 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-AlkamiDatabase.ps1 @@ -0,0 +1,107 @@ +Workflow Update-AlkamiDatabase { +<# +.SYNOPSIS + Database Migration + +.PARAMETER MigratePath + Path to migration exe and dlls +.PARAMETER WhatIf + Show planned execution without actually changing database +.PARAMETER Tee + Log output to a file in $MigratePath or $fallbackMigratePath +.PARAMETER MasterConnectionString + Connection string for Master Database +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Alkami generates this string manually, no user injection')] + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [string]$MigratePath = "C:\Temp\deploy\DatabaseMigration\", + [Parameter(Mandatory=$false)] + [switch]$WhatIf, + [Parameter(Mandatory=$false)] + [switch]$Tee, + [Parameter(Mandatory=$false)] + [string]$MasterConnectionString + ) + + $logLead = Get-LogLeadName + $DateTime = Get-Date -Format "MMddhhmm" + + $migrationPath = $MigratePath + # Old Folder name + $fallbackMigratePath = "C:\Temp\deploy\FluentMigrator\" + if (!(Test-Path -Path $migrationPath)) { + Write-Warning ("$logLead : Could not find path {0} -- defaulting to path {1}" -f $migrationPath, $fallbackMigratePath) + $migrationPath = $fallbackMigratePath + } + + $migrateExe = Join-Path -Path $migrationPath -ChildPath "Migrate.exe" + $masterDll = Join-Path -Path $migrationPath -ChildPath "Alkami.Tools.MasterDatabaseMigration.dll" + $tenantDll = Join-Path -Path $migrationPath -ChildPath "Alkami.Tools.TenantMigration.dll" + $tempPath = Split-Path -Path $migrationPath -Parent + + if (!(Test-Path $migrateExe)) { + throw ("$logLead : File not found {0}"-f $migrateExe) + } elseif (!(Test-Path $masterDll)) { + throw ("$logLead : File not found {0}"-f $masterDll) + } elseif (!(Test-Path $tenantDll)) { + throw ("$logLead : File not found {0}"-f $tenantDll) + } + + if (!($MasterConnectionString)) { + $MasterConnectionString = InlineScript { Get-MasterConnectionString } + } + + if ($null -ne $MasterConnectionString -and $MasterConnectionString -ne "REPLACEME") { + Write-Output "$logLead : Getting Tenants Using Connection String $MasterConnectionString" + $tenants = InlineScript { Get-CatalogsFromMaster $using:MasterConnectionString } + Write-Output ("$logLead : {0} Tenants Found" -f $tenants.Count) + } else { + throw "$logLead : Could Not Find a Valid AlkamiMaster Connection String in the machine.config and it was not passed as a parameter" + } + + if ($WhatIf.IsPresent -eq $true) { + $preview = "--preview" + } elseif ($WhatIf.IsPresent -eq $false) { + $preview = $null + } + + if ($Verbose.IsPresent -eq $true) { + $vString = "--verbose" + } elseif ($Verbose.IsPresent -eq $false) { + $vString = $null + } + + if ($MasterConnectionString) { + if ($Tee.IsPresent -eq $true) { + $masterLog = join-Path -Path $tempPath -childpath "MasterMigration_$DateTime.log" + $masterPath = "| Tee-Object -FilePath $masterLog" + } elseif ($Tee.IsPresent -eq $false) { + $masterPath = $null + } + + $masterMigration = "$migrateExe -c='$MasterConnectionString' -a='$masterDll' --db='sqlserver2008' --timeout=2500 '$vString' -task='migrate' $preview $masterPath" + Write-Information "$logLead : Executing master migration command: $masterMigration" + $masterResults = Invoke-Expression $masterMigration + Write-Output $masterResults + } + if ($tenants) { + foreach -parallel ($tenant in $tenants) { + if ($Tee.IsPresent -eq $true) { + $tenantLog = join-Path -Path $tempPath -childpath "$($tenant.catalog)_$DateTime.log" + $tenantPath = "| Tee-Object -FilePath $tenantLog" + } elseif ($Tee.IsPresent -eq $false) { + $tenantPath = $null + } + $tenantMigration = "$migrateExe -c='$($tenant.ConnectionString)' -a='$tenantDll' --db='sqlserver2008' --timeout=2500 '$vString' -task='migrate' $preview $tenantPath" + Write-Information "$logLead : Executing tenant migration command: $tenantMigration" + $tenantResults = Invoke-Expression $tenantMigration + + Write-Output $tenantResults + } + } + if ($Tee.IsPresent -eq $true) { + Write-Output ("`r`n$logLead : Writing Output to {0}`r`n" -f $tempPath) + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-Database.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-Database.ps1 new file mode 100644 index 0000000..95a5bed --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-Database.ps1 @@ -0,0 +1,57 @@ +function Update-Database { +<# +.SYNOPSIS + Update Alkami Database +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Alkami generates this string manually, no user injection')] + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$MigratePath, + [Parameter(Mandatory=$true)] + [string]$DatabaseString, + [Parameter(Mandatory=$true)] + [string]$DLLString, + [Parameter(Mandatory=$false)] + [switch]$WhatIf, + [Parameter(Mandatory=$false)] + [switch]$Tee + + ) + + $logLead = (Get-LogLeadName); + $DateTime = Get-Date -Format "MMddhhmmss" + + $migrateExe = Join-Path $migratePath "Migrate.exe" + $migrateDll = Join-Path $migratePath $DLLString + + if (!(Test-Path $migrateExe)){ + throw ("$logLead : File not found {0}"-f $migrateExe) + } elseif (!(Test-Path $migrateDll)){ + throw ("$logLead : File not found {0}"-f $migrateDll) + } + + if ($WhatIf.IsPresent -eq $true) { + $preview = "--preview" + } elseif ($WhatIf.IsPresent -eq $false) { + $preview = $null + } + + if ($verbose.IsPresent -eq $true) { + $vString = "--verbose" + } elseif ($verbose.IsPresent -eq $false) { + $vString = $null + } + + if ($tee.IsPresent -eq $true) { + $logs = join-Path -Path $migratePath -childpath "Migrate$DateTime.log" + $logsOutput = "| Tee-Object -FilePath $logs" + Write-Output ("$logLead : Logs Output '{0}'" -f $logs ) + } elseif ($tee.IsPresent -eq $false) { + $logsOutput = $null + } + + $Migrate = "$migrateExe -c='$DatabaseString' -a='$migrateDll' --db='sqlserver2012' --timeout=2500 '$vString' -task='migrate' $preview $logsOutput" + Invoke-Expression $Migrate +} + diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.ps1 new file mode 100644 index 0000000..4f3b2f9 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.ps1 @@ -0,0 +1,106 @@ +function Update-S3BucketTags { + + <# + .SYNOPSIS + Updates the tags on the specified S3 Bucket. Returns True if tags update as instructed. + + .DESCRIPTION + Updates the tags on the specified S3 Bucket. The existing Write-S3BucketTagging over-writes existing tags. + This function is just a wrapper to allow for additive behavior if desired. + + .PARAMETER BucketName + [string] The name of the bucket to update. + + .PARAMETER ProfileName + [string] The AWS profile to use, generally temp-. + + .PARAMETER Region + [string] Specify the region of the bucket. + + .PARAMETER TagSet + [string[]] The tags to add to the bucket, in format "Key1,Value1", "Key2,Value2" etc. + + .PARAMETER OverWrite + [switch] Indicate if the TagSet is adding to, or replacing, existing tags. + + .EXAMPLE + Update-S3BucketTags -BucketName bucket1-east -ProfileName temp-prod -Region us-east-1 -TagSet "TestKey,TestValue" + + This example will add TestKey:TestValue to the existing tags on bucket bucket1-east + + .EXAMPLE + Update-S3BucketTags -BucketName bucket1-east -ProfileName temp-prod -Region us-east-1 -TagSet "TestKey,TestValue", "TestKey2,TestValue2" -Overwrite + + This example will replace any existing keys on bucket1-east with the supplied tags: + TestKey:TestValue + TestKey2:TestValue2 + #> + + [cmdletbinding(DefaultParameterSetName = "Add")] + param ( + [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = "Add")] + [ValidateNotNullorEmpty()] + [string] + $BucketName, + [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = "Add")] + [ValidateNotNullorEmpty()] + [string] + $ProfileName, + [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = "Add")] + [ValidateNotNullorEmpty()] + [string] + $Region, + [Parameter(Mandatory = $true)] + [Parameter(ParameterSetName = "Add")] + [ValidateNotNullorEmpty()] + [string[]] + $TagSet, + [Parameter(Mandatory = $false)] + [switch] + $OverWrite + ) + $logLead = (Get-LogLeadName) + + Import-AWSModule + + $awsTagSet = @() + + if (!($OverWrite)) { + try { + $currentTags = @(Get-S3BucketTagging -BucketName $BucketName -ProfileName $ProfileName -Region $Region) + } catch { + $errorMessage = $_.Exception.Message + if ($errorMessage -like "*The bucket you are attempting to access must be addressed using the specified endpoint*") { + [string]$errorMessage = "The region specified, $Region, is incorrect for bucket $BucketName" + } + Write-Error "$logLead : $errorMessage" + return + } + + if (-not (Test-IsCollectionNullOrEmpty $currentTags)) { + foreach ($tag in $currentTags) { + $awsTagSet += @{ Key = $tag.Key; Value = $tag.Value; } + } + } + } + + foreach ($set in $TagSet) { + $splits = $set.Split(',') + $awsTagSet += @{ Key = $splits[0]; Value = $splits[1]; } + } + + # Write new tagset to the bucket + try { + Write-S3BucketTagging -BucketName $BucketName -ProfileName $ProfileName -TagSet $awsTagSet -Region $Region + } catch { + $errorMessage = $_.Exception.Message + if ($errorMessage -like "*The bucket you are attempting to access must be addressed using the specified endpoint*") { + [string]$errorMessage = "The region specified, [$Region], is incorrect for bucket [$BucketName]" + } + Write-Error "$logLead : $errorMessage" + return + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.tests.ps1 new file mode 100644 index 0000000..9a969f3 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-S3BucketTags.tests.ps1 @@ -0,0 +1,99 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Update-S3BucketTags.tests' } +Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} +Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + +Describe "Update-S3BucketTags" { + + Context "When a required parameter is null" { + It "Throws an Exception if the bucket name is blank" { + { Update-S3BucketTags -BucketName '' -ProfileName aTest -Region us-east-1 -TagSet "TestTag1,TestValue1" } | Should -Throw + } + + It "Throws an Exception if the ProfileName is blank" { + { Update-S3BucketTags -BucketName aBucket -ProfileName '' -Region us-east-1 -TagSet "TestTag1,TestValue1" } | Should -Throw + } + + It "Throws an Exception if the Region is blank" { + { Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region '' -TagSet "TestTag1,TestValue1" } | Should -Throw + } + + It "Throws an Exception if the TagSet is blank" { + { Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet '' } | Should -Throw + } + } + + Context "Error Handling" { + It "Writes an error if the region is incorrect for the given bucket" { + Mock -CommandName Get-S3BucketTagging -ModuleName $moduleForMock ` + -MockWith { throw "The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint."} + + Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet "Key1,Value1" + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*The region specified, *, is incorrect for bucket *" } + + } + + It "Writes an error if there are issues reaching the bucket to retrieve tags" { + Mock -CommandName Get-S3BucketTagging -ModuleName $moduleForMock -MockWith { throw "Oh no, an error." } + + Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet "Key1,Value1" + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -match "Oh no, an error."} + } + + It "Writes an error if there are issues writing tags to the bucket" { + Mock -CommandName Get-S3BucketTagging -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Write-S3BucketTagging -MockWith { throw "Oh no, an error." } + + Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet "Key1,Value1" + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -match "Oh no, an error." } + Assert-MockCalled -CommandName Get-S3BucketTagging -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled -CommandName Write-S3BucketTagging -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + } + } + + Context "Adds tags to the bucket" { + It "Adds additional tags" { + $mockS3BucketTagging = { + $tag = [Amazon.S3.Model.Tag]::new() + $tag.Set_Key('ATestKey') + $tag.Set_Value('ATestValue') + return $tag + } + + Mock -CommandName Get-S3BucketTagging -ModuleName $moduleForMock -MockWith $mockS3BucketTagging + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Write-S3BucketTagging -ModuleName $moduleForMock -MockWith {} + + Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet "Key1,Value1" + + Assert-MockCalled Get-S3BucketTagging -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled Test-IsCollectionNullOrEmpty -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled Write-S3BucketTagging -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + + } + + It "Overwrites existing tags" { + Mock -CommandName Get-S3BucketTagging -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-S3BucketTagging -ModuleName $moduleForMock -MockWith {} + + Update-S3BucketTags -BucketName aBucket -ProfileName aTest -Region us-east-1 -TagSet "Key1,Value1", "Key2,Value2" -OverWrite + + Assert-MockCalled Get-S3BucketTagging -Times 0 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled Test-IsCollectionNullOrEmpty -Times 0 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled Write-S3BucketTagging -Times 1 -Exactly -ModuleName $moduleForMock -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-StagedConfigurationValues.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-StagedConfigurationValues.ps1 new file mode 100644 index 0000000..238f0b6 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-StagedConfigurationValues.ps1 @@ -0,0 +1,69 @@ +function Update-StagedConfigurationValues { +<# +.SYNOPSIS + Sets the appropriate configuration values in various staged *.config files + +.PARAMETER EnvironmentKey + Environment name used in NewRelic AppName + +.PARAMETER StagedFilePath + Path to staged deployment files + +.PARAMETER DeployedFilePath + Path to deployed files + +.DESCRIPTION + After the build is staged for copy on each server this function calls functions which either set + static defaults, or copies values from the prior config file in to the new one from the build. This + allows us to retain settings from one release to the next while still incorporating changes which come from development + +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$EnviromentKey, + + [Parameter(Mandatory = $false)] + [string]$StagedFilePath = "C:\temp\Deploy\Orb\", + + [Parameter(Mandatory = $false)] + [string]$DeployedFilePath + ) + + $logLead = Get-LogLeadName + + if (Test-StringIsNullOrEmpty -Value $DeployedFilePath) { + $DeployedFilePath = Get-OrbPath + } + + $pathError = "$logLead : Path {0} Does Not Exist!" + @($StagedFilePath, $DeployedFilePath) | ForEach-Object { + + if (!(Test-Path $_ -PathType Container)) { + Write-Warning ($pathError -f $_) + throw ($pathError -f $_) + } + } + + # Get Configuration Files That Need Updates + Write-Host ("$logLead : Looking for Configuration Files in {0}" -f $filePath) + $configFiles = @() + $configFiles += Get-ConfigurationFiles $StagedFilePath $true + + # Also get Configuration Files that are not prefaced with new.* + # Some are released this way, and this will also cover any new ones that get thrown in the mix + $configFiles += Get-ConfigurationFiles $StagedFilePath $false + + # Set Standard Configurations + Write-Verbose "$logLead : Calling Set-StaticConfigValues" + Set-StaticConfigValues $configfiles $StagedFilePath $DeployedFilePath + + # Update NewRelic App Names + Write-Verbose ("$logLead : Calling Set-NewRelicAppName With Environment Key '{0}'" -f $EnviromentKey) + Set-NewRelicAppName -enviroment "$EnviromentKey" -configFiles $configfiles + + # Rename the Files + Write-Verbose "$logLead : Calling Rename-TemporaryConfigFiles" + Rename-TemporaryConfigFiles $configfiles +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Update-TrustedSites.ps1 b/Modules/Alkami.DevOps.Operations/Public/Update-TrustedSites.ps1 new file mode 100644 index 0000000..cd7d080 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Update-TrustedSites.ps1 @@ -0,0 +1,82 @@ +function Update-TrustedSites { +<# +.SYNOPSIS + Updates IE's Trusted Sites for the Current User and Disables the IE Popup Blocker + +.DESCRIPTION + Updates IE's Trusted Sites for the Current User and Disables the IE Popup Blocker + +.EXAMPLE + Update-TrustedSites + +PS C:\Users\dsage> Update-TrustedSites +[Update-TrustedSites] : Adding URL *.bethpagefcu.com to Trusted Zone +[Update-TrustedSites] : Adding URL *.bethpagefcu.com to Escaped Domains +[Update-TrustedSites] : Adding URL *.secumd.org to Trusted Zone +[Update-TrustedSites] : Adding URL *.secumd.org to Escaped Domains +[Update-TrustedSites] : Adding URL *.bellco.org to Trusted Zone +[Update-TrustedSites] : Adding URL *.bellco.org to Escaped Domains +#> + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + $trustedSitesRegistryKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains" + $escDomainsRregistryKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains" + $popupBlockerRegistryKey = "HKCU:\Software\Microsoft\Internet Explorer\New Windows" + $popupBlockerRegistrySetting = "PopupMgr" + + $trustedDWord = 2 + + $mgr = New-Object Microsoft.Web.Administration.ServerManager + $clientSites = ($mgr.Sites | Where-Object {$_.Applications["/"].VirtualDirectories["/"].PhysicalPath -like (join-Path(Get-OrbPath) "WebClient*")}) + + if (Test-IsCollectionNullOrEmpty $clientSites) { + + Write-Warning ("$logLead : Could not find any WebClient sites on this server") + return + } + + [string[]]$addedSites = @() + foreach ($site in ($clientSites | Sort-Object | Get-Unique)) { + + [array]$sslBindings = ($site.Bindings | Where-Object {$_.Protocol -eq "https"}) + + if (Test-IsCollectionNullOrEmpty $sslBindings) { + + Write-Warning ("$logLead : Could not find SSL Binding for Site {0}" -f $site.Name) + continue + } + + foreach ($sslBinding in $sslBindings) { + + $hostSegments = $sslBinding.Host.Split(".") + $hostSegments[0] = "*" + $trustedSegment = $hostSegments -join "." + $registryKeyName = ($hostSegments | Select-Object -Skip 1) -join "." + + if ($addedSites.Contains($registryKeyName)) { + + # We've already hit this one + continue + } + + $addedSites += $registryKeyName + Write-Host ("$logLead : Adding URL {0} to Trusted Zone" -f $trustedSegment) + (New-Item -Path $trustedSitesRegistryKey -ItemType File -Name "$registryKeyName" -ErrorAction SilentlyContinue) | Out-Null + (Set-ItemProperty -Path $trustedSitesRegistryKey\$registryKeyName -Name "https" -Value $trustedDWord -ErrorAction SilentlyContinue) | Out-Null + + Write-Host ("$logLead : Adding URL {0} to Escaped Domains" -f $trustedSegment) + (New-Item -Path $escDomainsRregistryKey -ItemType File -Name "$registryKeyName" -ErrorAction SilentlyContinue) | Out-Null + (Set-ItemProperty -Path $escDomainsRregistryKey\$registryKeyName -Name "https" -Value $trustedDWord -ErrorAction SilentlyContinue) | Out-Null + } + } + + # Disable Popup Blocker + if ((Get-ItemProperty $popupBlockerRegistryKey $popupBlockerRegistrySetting).PopupMgr -eq 1) { + + Write-Host ("$logLead : Disabling IE Popup Blocker") + (Set-ItemProperty -Path $popupBlockerRegistryKey -Name $popupBlockerRegistrySetting -Value 0 -ErrorAction SilentlyContinue) | Out-Null + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.ps1 b/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.ps1 new file mode 100644 index 0000000..d990a12 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.ps1 @@ -0,0 +1,87 @@ +function Wait-Route53ChangeStatus { + +<# +.SYNOPSIS + Attempt to wait for the specified Route53 Hosted Zone change to exhibit the specified Route53 change status. + +.DESCRIPTION + Attempt to wait for the specified Route53 Hosted Zone change to exhibit the specified Route53 change status. Will write an error if a timeout occurs. + Refer to https://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeInfo.html + +.PARAMETER ChangeId + [string] The ID of the Route53 Hosted Zone change. + +.PARAMETER AwsProfileName + [string] The AWS profile where the Route53 Hosted Zone is located (e.g. 'temp-prod'). + +.PARAMETER ChangeStatus + [string] The desired Route53 Hosted Zone change status. Default is "INSYNC". + +.PARAMETER DelaySeconds + [byte] The number of seconds to delay between each change status poll request. Range is from 5 to 30 seconds. Default is 10 seconds. + +.PARAMETER DelaySeconds + [byte] The number of seconds to wait before timing the operation out. Range is from 30 to 900 seconds. Default is 300 seconds. + +.EXAMPLE + Wait-Route53ChangeStatus -ChangeId 'CHANGE1234' -AwsProfileName 'temp-prod' -ChangeStatus 'INSYNC' -TimeoutSeconds 300 -DelaySeconds 10 + +[Wait-Route53ChangeStatus] : Route53 Change ID 'CHANGE1234' has status 'INSYNC' after 82.0345 seconds. +#> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ChangeId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $AwsProfileName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $ChangeStatus = "INSYNC", + + [Parameter(Mandatory = $false)] + [ValidateRange(5, 30)] + [byte] $DelaySeconds = 10, + + [Parameter(Mandatory = $false)] + [ValidateRange(30, 900)] + [uint16] $TimeoutSeconds = 300 + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule + + # Start the timer. + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + + # Retrieve the initial status. Loop until we get the target status or run out of time. + $actualStatus = ( Get-R53Change -Id $ChangeId -ProfileName $AwsProfileName ).Status + while ( ( $actualStatus -ne $ChangeStatus ) -and ( $stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds )) { + + Write-Verbose ( "{0} : Route53 Change ID '{1}' has status '{2}' after {3} seconds." -f $logLead, $ChangeId, $actualStatus, $stopwatch.Elapsed.TotalSeconds ) + + # Delay slightly to allow the change time to apply. + Start-Sleep -Seconds $DelaySeconds + + # Poll the status again. + $actualStatus = ( Get-R53Change -Id $ChangeId -ProfileName $AwsProfileName ).Status + } + + # Stop the timer. + $stopwatch.Stop() + + # If we timed out, let the user know. + if ( $stopwatch.Elapsed.TotalSeconds -ge $TimeoutSeconds ) { + + Write-Error ( "{0} : Timeout after {1} seconds waiting on Route53 change ID '{2}' to have status '{3}'" -f $logLead, $stopwatch.Elapsed.TotalSeconds, $ChangeId, $ChangeStatus ) + return $null + } + + # If we didn't time out, let the user know the final state. + Write-Host ( "{0} : Route53 Change ID '{1}' has status '{2}' after {3} seconds." -f $logLead, $ChangeId, $actualStatus, $stopwatch.Elapsed.TotalSeconds ) +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.tests.ps1 b/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.tests.ps1 new file mode 100644 index 0000000..1dd672c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Wait-Route53ChangeStatus.tests.ps1 @@ -0,0 +1,103 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Wait-Route53ChangeStatus" { + + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Wait-Route53ChangeStatus.tests' } + + Context "Input Validation" { + + It "Change ID Should Not Be Null" { + + { Wait-Route53ChangeStatus -ChangeId $null } | Should -Throw + } + + It "Change ID Should Not Be Empty" { + + { Wait-Route53ChangeStatus -ChangeId '' } | Should -Throw + } + + It "AWS Profile Name Should Not Be Null" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName $null } | Should -Throw + } + + It "AWS Profile Name Should Not Be Empty" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName '' } | Should -Throw + } + + It "Change Status Should Not Be Null" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -ChangeStatus $null } | Should -Throw + } + + It "Change Status Should Not Be Empty" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -ChangeStatus '' } | Should -Throw + } + + It "Delay Seconds Should Be An Integer" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -DelaySeconds 'Test' } | Should -Throw + } + + It "Delay Seconds Should Respect Minimum" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -DelaySeconds 0 } | Should -Throw + } + + It "Delay Seconds Should Respect Maximum" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -DelaySeconds 9001 } | Should -Throw + } + + It "Timeout Seconds Should Be An Integer" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -TimeoutSeconds 'Test' } | Should -Throw + } + + It "Timeout Seconds Should Respect Minimum" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -TimeoutSeconds 0 } | Should -Throw + } + + It "Timeout Seconds Should Respect Maximum" { + + { Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -TimeoutSeconds 9001 } | Should -Throw + } + } + + Context "Result Validation" { + + It "Should Return Without Error If Change Status Matches Desired" { + + Mock -CommandName Get-R53Change -ModuleName $moduleForMock -MockWith { return @{ 'Status' = 'Test' } } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -ChangeStatus 'Test' + + Assert-MockCalled -CommandName Get-R53Change -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Return With Error If Timeout Occurs" { + + Mock -CommandName Get-R53Change -ModuleName $moduleForMock -MockWith { return @{ 'Status' = 'Test' } } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + Write-Host "Testing timeout of 30 seconds; be patient." + Wait-Route53ChangeStatus -ChangeId 'Test' -AwsProfileName 'Test' -ChangeStatus 'FailTest' -TimeoutSeconds 30 -DelaySeconds 10 + + Assert-MockCalled -CommandName Get-R53Change -Times 4 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Timeout" } + } + } +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Write-ChocoCommandsToFile.ps1 b/Modules/Alkami.DevOps.Operations/Public/Write-ChocoCommandsToFile.ps1 new file mode 100644 index 0000000..e6e313d --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Write-ChocoCommandsToFile.ps1 @@ -0,0 +1,68 @@ +function Write-ChocoCommandsToFile { +<# +.SYNOPSIS + Writes a list of chocolatey package commands out to a file. Force parameter optional. + +.NOTES + The default output path is a "known" file location intended to work with Install-MicroservicesWithMigrations +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object[]]$ChocoPackages, + + [Parameter(Mandatory = $false)] + [string]$OutputPath = "C:/temp/deploy/chocoInstallCommands.ps1", + + [Parameter(Mandatory = $false)] + [string]$Environment, + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [Alias("migrate", "m")] + [switch]$RunMigrations, + + [Parameter(Mandatory = $false)] + [switch]$DisableMicroservices + ) + + $forceFlag = "" + if ($Force.IsPresent) { + $forceFlag = "f" + } + $output = "`$env:MaxMigrationJobs = 15`n"; + + if (!$ChocoPackages) { + $output += "Write-Host ""No chocolatey packages to install. Done!""" + } else { + # Randomize the order of the installed chocolatey packages, to better guarantee that there is at least one microservice running at a time when several installs are running. + $ChocoPackages = Get-Random -InputObject $ChocoPackages -Count ([int]::MaxValue) + + $counter = 1 + $numPackages = $ChocoPackages.count + foreach ($package in $ChocoPackages) { + # Get NewRelic params + $paramsSwitch = Get-ChocolateyParameterString $package -env $Environment -migrate $RunMigrations.IsPresent -start $false; + + $output += "Write-Host `"Installing Package ($counter/$numPackages):`";" + $output += "choco upgrade $($package.Name) -y$forceFlag --version $($package.Version) $paramsSwitch;`n" + + # If we have just installed the notification service, guarantee that it is running after the install to deal with migrations. + $notificationServiceName = "Alkami.MicroServices.Notifications.Service.Host"; + if ($package.Name -like $notificationServiceName) { + $output += "`$service = (Get-ServiceByChocoName $notificationServiceName);`nif((`$service -ne `$null) -and (`$service.Name -ne `$null)){ Start-AlkamiService -serviceName `$service.Name; }`n" + } + + $counter++ + } + } + + if ($DisableMicroservices.IsPresent) { + $output += "(Disable-Microservices -Verbose);`n" + } + + Set-Content -Path $OutputPath -Value $output -Force +} diff --git a/Modules/Alkami.DevOps.Operations/Public/Write-TcpSocketStreamWriter.ps1 b/Modules/Alkami.DevOps.Operations/Public/Write-TcpSocketStreamWriter.ps1 new file mode 100644 index 0000000..3575b0e --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Public/Write-TcpSocketStreamWriter.ps1 @@ -0,0 +1,17 @@ +function Write-TcpSocketStreamWriter { + <# +.SYNOPSIS + Writes String to the Stream Writer buffer. Execustes WriteLine() on System.IO.StreamWriter. +.EXAMPLE + Write-TcpSocketStreamWriter -Writer $writer -Value $metric +.INPUTS + Writer + Value +#> + [cmdletbinding()] + param( + $Writer, + $Value + ) + $Writer.WriteLine($Value) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Resources/PackageFiltering-min.json b/Modules/Alkami.DevOps.Operations/Resources/PackageFiltering-min.json new file mode 100644 index 0000000..0d3601c --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Resources/PackageFiltering-min.json @@ -0,0 +1 @@ +{"mappings":[{"PackageId":"Alkami.MicroServices.AccountBalanceSync1.Processor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Account Balance Sync 1 Processor Service"},{"PackageId":"Alkami.MicroServices.AccountBalanceSync2.Processor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Account Balance Sync 2 Processor Service"},{"PackageId":"Alkami.MicroServices.ACHStopPay.SymConnect.Service.Host","ProviderType":"ACHStopPay","ProviderName":"SymConnect ACH Stop Pay Provider"},{"PackageId":"Alkami.MicroServices.AddressValidation.SmartyStreets.Host","ProviderType":"AddressValidation","ProviderName":"Smarty Streets Address Validation Provider"},{"PackageId":"Alkami.MicroServices.AggregationProviders.Yodlee.Host","ProviderType":"Aggregation","ProviderName":"Yodlee Aggregation Provider"},{"PackageId":"Alkami.MicroServices.Alerts.Patelco.Service.Host","ProviderType":"Nag","ProviderName":"Patelco Alert Microsevice"},{"PackageId":"Alkami.MicroServices.Authentication.AD.Service.Host","ProviderType":"Authentication","ProviderName":"Active Directory"},{"PackageId":"Alkami.MicroServices.Authentication.Entrust.Service.Host","ProviderType":"Authentication","ProviderName":"Entrust Authentication Provider"},{"PackageId":"Alkami.MicroServices.Authentication.External.Service.Host","ProviderType":"Authentication","ProviderName":"External Authentication Provider"},{"PackageId":"Alkami.MicroServices.Authentication.Static.Service.Host","ProviderType":"Authentication","ProviderName":"Static Authentication Provider"},{"PackageId":"Alkami.MicroServices.Authentication.Workflow.Service.Host","ProviderType":"AuthenticationWorkflow","ProviderName":"Alkami Authentication Workflow Provider"},{"PackageId":"Alkami.MicroServices.AutoBiller.Static.Service.Host","ProviderType":"AutoBiller","ProviderName":"Static AutoBiller Provider"},{"PackageId":"Alkami.MicroServices.AutoBiller.Symitar.Service.Host","ProviderType":"AutoBiller","ProviderName":"Symitar AutoBiller Provider"},{"PackageId":"Alkami.MicroServices.BCUBenefits.Service.Host","ProviderType":"CustomBenefitsBcu","ProviderName":"BCU Benefits Provider"},{"PackageId":"Alkami.MicroServices.BillPayProviders.CheckFree.Service.Host","ProviderType":"BillPay","ProviderName":"CheckFree Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.CheckFreeSB.Service.Host","ProviderType":"BillPay","ProviderName":"CheckFree Small Business Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.FIS.MACU.Service.Host","ProviderType":"BillPay","ProviderName":"FIS MACU Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.FIS.Service.Host","ProviderType":"BillPay","ProviderName":"FIS Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.IPay.Service.Host","ProviderType":"BillPay","ProviderName":"IPay Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.Payveris.Service.Host","ProviderType":"BillPay","ProviderName":"Payveris Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.PSCU.Service.Host","ProviderType":"BillPay","ProviderName":"PSCU Bill Pay"},{"PackageId":"Alkami.MicroServices.BillPayProviders.Static.Service.Host","ProviderType":"BillPay","ProviderName":"Static Bill Pay"},{"PackageId":"Alkami.MicroServices.BusinessReport.Notifications.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Business Report Notifications Processor Service"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Bcu.Host","ProviderType":"CardManagement","ProviderName":"BCU Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Connex.Host","ProviderType":"CardManagement","ProviderName":"Connex Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Corelation.Host","ProviderType":"CardManagement","ProviderName":"Corelation Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.DNA.Host","ProviderType":"CardManagement","ProviderName":"DNA Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Ondot.Host","ProviderType":"CardManagement","ProviderName":"Ondot Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Pscu.Host","ProviderType":"CardManagement","ProviderName":"PSCU Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Spectrum.Host","ProviderType":"CardManagement","ProviderName":"Spectrum Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Static.Host","ProviderType":"CardManagement","ProviderName":"Static Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.SymConnect.Host","ProviderType":"CardManagement","ProviderName":"SymConnect Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.UltraData.Host","ProviderType":"CardManagement","ProviderName":"UltraData Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.Universal.Host","ProviderType":"CardManagement","ProviderName":"Universal Card Management Provider"},{"PackageId":"Alkami.MicroServices.CardManagementProviders.XP.Host","ProviderType":"CardManagement","ProviderName":"XP Card Management Provider"},{"PackageId":"Alkami.MicroServices.CheckOrderProviders.Deluxe.Host","ProviderType":"CheckOrder","ProviderName":"DeluxeCheckReorderProvider"},{"PackageId":"Alkami.MicroServices.CheckOrderProviders.Static.Host","ProviderType":"CheckOrder","ProviderName":"Static Check Order Provider"},{"PackageId":"Alkami.MicroServices.CivicRewards.Service.Host","ProviderType":"CivicRewards","ProviderName":"CivicRewards"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.CoA.Service.Host","ProviderType":"Core","ProviderName":"CoA Core Provider"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.CoreApi.Service.Host","ProviderType":"Core","ProviderName":"CoreApi Core Provider"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.ESB.OCCU.Service.Host","ProviderType":"Core","ProviderName":"OCCU ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.Miser.Service.Host","ProviderType":"Miser Core Provider","ProviderName":"Core"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.Spectrum.Service.Host","ProviderType":"Core","ProviderName":"Spectrum Core Provider"},{"PackageId":"Alkami.MicroServices.Core.AccountUpdate.SymConnect.Service.Host","ProviderType":"Core","ProviderName":"SymConnect Core Provider MS"},{"PackageId":"Alkami.MicroServices.Core.CCM.Service.Host","ProviderType":"Core","ProviderName":"CCM Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CIF2020.Registration.Service.Host","ProviderType":"Core","ProviderName":"CIF2020CoreProvider"},{"PackageId":"Alkami.MicroServices.Core.CIF2020.Service.Host","ProviderType":"Core","ProviderName":"CIF2020CoreProvider"},{"PackageId":"Alkami.MicroServices.Core.CoA.CCM.Force.Service.Host","ProviderType":"Core","ProviderName":"CoA CCM Salesforce Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CoA.CCM.Service.Host","ProviderType":"Core","ProviderName":"CoA CCM Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CoA.Registration.Service.Host","ProviderType":"Core","ProviderName":"CoA Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CoA.Service.Host","ProviderType":"Core","ProviderName":"CoA Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CoreApi.Registration.Service.Host","ProviderType":"Core","ProviderName":"CoreApi Core Provider"},{"PackageId":"Alkami.MicroServices.Core.CoreApi.Service.Host","ProviderType":"Core","ProviderName":"CoreApi Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Corelation.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Dynamic.Service.Host","ProviderType":"Core","ProviderName":"Dynamic Core Provider"},{"PackageId":"Alkami.MicroServices.Core.ESB.CUFX.Service.Host","ProviderType":"Core","ProviderName":"CUFX ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.ESB.OCCU.Service.Host","ProviderType":"Core","ProviderName":"OCCU ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.LoanPayoffCalculator.Corelation.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MicroServices.Core.LoanPayoffCalculator.ESB.OCCU.Service.Host","ProviderType":"Core","ProviderName":"OCCU ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.LoanPayoffCalculator.Miser.Service.Host","ProviderType":"Core","ProviderName":"Miser Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Miser.Service.Host","ProviderType":"Core","ProviderName":"Miser Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Registration.ESB.CUFX.Service.Host","ProviderType":"Core","ProviderName":"CUFX ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Registration.ESB.OCCU.Service.Host","ProviderType":"Core","ProviderName":"OCCU ESB Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Registration.Miser.Service.Host","ProviderType":"Core","ProviderName":"Miser Core Provider"},{"PackageId":"Alkami.MicroServices.Core.RTA.Service.Host","ProviderType":"Core","ProviderName":"RTA Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Silverlake.Registration.Service.Host","ProviderType":"Core","ProviderName":"SilverlakeCoreProvider"},{"PackageId":"Alkami.MicroServices.Core.Silverlake.Service.Host","ProviderType":"Core","ProviderName":"SilverlakeCoreProvider"},{"PackageId":"Alkami.MicroServices.Core.Spectrum.Service.Host","ProviderType":"Core","ProviderName":"Spectrum Core Provider"},{"PackageId":"Alkami.MicroServices.Core.Static.Service.Host","ProviderType":"Core","ProviderName":"Static Core Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.Corelation.Host","ProviderType":"CourtesyPay","ProviderName":"Corelation Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.Dynamic.Host","ProviderType":"CourtesyPay","ProviderName":"Dynamic Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.ESB.OTS.Service.Host","ProviderType":"CourtesyPay","ProviderName":"ESB OTS Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.Spectrum.Host","ProviderType":"CourtesyPay","ProviderName":"Spectrum Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.SymConnect.Service.Host","ProviderType":"CourtesyPay","ProviderName":"SymConnect Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.UltraData.Service.Host","ProviderType":"CourtesyPay","ProviderName":"UltraData Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CourtesyPayProviders.XP.Host","ProviderType":"CourtesyPay","ProviderName":"XP Courtesy Pay Provider"},{"PackageId":"Alkami.MicroServices.CustomCore.Miser.Occu.Service.Host","ProviderType":"CustomCore","ProviderName":"Custom Core Miser Occu Service"},{"PackageId":"Alkami.MicroServices.EM.BadProcessor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management BadProcessor Processor Service"},{"PackageId":"Alkami.MicroServices.EM.BlockedACH.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management BlockedACHAccountAlert Processor Service"},{"PackageId":"Alkami.MicroServices.EM.BlockedACH.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management BlockedACHAccountAlert Processor Service"},{"PackageId":"Alkami.MicroServices.EM.ExternalTransfer.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management ETAP Service"},{"PackageId":"Alkami.MicroServices.EM.ExternalTransferCancelled.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management ExternalTransferCancelled Processor Service"},{"PackageId":"Alkami.MicroServices.EM.ExtTransferCancelled.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management ExternalTransferCancelled Processor Service"},{"PackageId":"Alkami.MicroServices.EM.TransferFailed.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management TransferFailed Processor Service"},{"PackageId":"Alkami.MicroServices.EM.TransferFailed.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management TransferFailed Processor Service"},{"PackageId":"Alkami.MicroServices.EM.TransferSucceeded.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management TransferSucceeded Processor Service"},{"PackageId":"Alkami.MicroServices.EM.TransferSucceeded.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management TransferSucceeded Processor Service"},{"PackageId":"Alkami.Microservices.EventManagement.Cleanup.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Cleanup Service"},{"PackageId":"Alkami.MicroServices.EventManagement.MMAP.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management MACU MyStyle Alert Processor Service"},{"PackageId":"Alkami.MicroServices.EventManagement.OAP.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Ondot Alert Processor Service"},{"PackageId":"Alkami.MicroServices.EventManagement.SAP.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Security Alert Processor Service"},{"PackageId":"Alkami.MicroServices.EventManagement.VAP.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Visa Authorization Processor Service"},{"PackageId":"Alkami.MicroServices.Fact.Desert.Service.Host","ProviderType":"Fact Provider","ProviderName":"Alkami.MicroServices.Fact.Desert"},{"PackageId":"Alkami.MicroServices.Fact.Miser.Occu.Service.Host","ProviderType":"Fact Provider","ProviderName":"Miser OCCU Fact Provider"},{"PackageId":"Alkami.MicroServices.Features.Beacon.Host","ProviderType":"Beacon","ProviderName":null},{"PackageId":"Alkami.MicroServices.FicoScore.Service.Host","ProviderType":"CreditScore","ProviderName":"FICO Credit Score Provider"},{"PackageId":"Alkami.MicroServices.Iav.Service.Host","ProviderType":"Iav","ProviderName":"Iav Service"},{"PackageId":"Alkami.MicroServices.Iav.Yodlee.Service.Host","ProviderType":"Iav","ProviderName":"Iav Yodlee Service"},{"PackageId":"Alkami.MicroServices.ICCUBusinessCardManagement.Service.Host","ProviderType":"ICCUBusinessCardManagement","ProviderName":"ICCUBusinessCardManagement"},{"PackageId":"Alkami.MicroServices.LinkedAccounts.Service.Host","ProviderType":"LinkedAccounts","ProviderName":"LinkedAccount Service"},{"PackageId":"Alkami.MicroServices.MacuRewards.Service.Host","ProviderType":"CustomMacuRewards","ProviderName":"Macu Rewards Provider"},{"PackageId":"Alkami.MicroServices.MessageCenter.Service.Host","ProviderType":"MessageCenterProvider","ProviderName":"AlkamiMessageCenterDynamicProvider"},{"PackageId":"Alkami.Microservices.ODPP.Corelation.Service.Host","ProviderType":"OverdraftProtection","ProviderName":"Corelation Overdraft Protection Provider"},{"PackageId":"Alkami.Microservices.ODPP.Dynamic.Service.Host","ProviderType":"OverdraftProtection","ProviderName":"Dynamic Overdraft Service"},{"PackageId":"Alkami.Microservices.ODPP.Patelco.Service.Host","ProviderType":"OverdraftProtection","ProviderName":"Patelco Overdraft Service"},{"PackageId":"Alkami.MicroServices.ODPP.SymConnect.Service.Host","ProviderType":"OverdraftProtection","ProviderName":"SymConnect Overdraft Protection Provider"},{"PackageId":"Alkami.MicroServices.ODPP.UltraData.Service.Host","ProviderType":"OverdraftProtection","ProviderName":"UltraData Overdraft Protection Provider"},{"PackageId":"Alkami.MicroServices.Payroll.SandiaLabs.Service.Host","ProviderType":"PayrollDistribution","ProviderName":"SandiaLabs Payroll Distribution Provider"},{"PackageId":"Alkami.MicroServices.Payroll.SymConnect.Service.Host","ProviderType":"PayrollDistribution","ProviderName":"SymConnect Payroll Distribution Provider"},{"PackageId":"Alkami.MicroServices.Processor.ApiPassthrough.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Api Passthrough Service"},{"PackageId":"Alkami.MicroServices.Processor.Flux.Reporting.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Flux Reporting Processor Service"},{"PackageId":"Alkami.MicroServices.Processor.RemoteDepositCompletion.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management RemoteDepositCompletion Processor Service"},{"PackageId":"Alkami.MicroServices.Processor.RevokeToken.PasswordChanged.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management RevokeToken.PasswordChanged Processor Service"},{"PackageId":"Alkami.MicroServices.Processor.RevokeToken.UsernameChanged.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management RevokeToken.UsernameChanged Processor Service"},{"PackageId":"Alkami.MicroServices.Processor.RIMT.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Resolve Inactive Message Threads Processor Service"},{"PackageId":"Alkami.MicroServices.QuickApply.CoA.Service.Host","ProviderType":"QuickApply","ProviderName":"Alkami.MicroServices.QuickApply.CoA Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.DNA.Service.Host","ProviderType":"QuickApply","ProviderName":"Alkami.MicroServices.QuickApply.DNA Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.Dynamic.Service.Host","ProviderType":"QuickApply","ProviderName":"Dynamic Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.ESB.Desert.Service.Host","ProviderType":"QuickApply","ProviderName":"DSFCU Quick Apply Provider"},{"PackageId":"Alkami.Microservices.QuickApply.Esb.Occu.Service.Host","ProviderType":"QuickApply","ProviderName":"ESB OCCU Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.MeridianLink.Service.Host","ProviderType":"QuickApply","ProviderName":"Meridian Link Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.Miser.Service.Host","ProviderType":"QuickApply","ProviderName":"Miser Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.Service.Host","ProviderType":"QuickApply","ProviderName":"Quick Apply Service"},{"PackageId":"Alkami.MicroServices.QuickApply.Spectrum.Service.Host","ProviderType":"QuickApply","ProviderName":"Spectrum Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.SymConnect.Service.Host","ProviderType":"QuickApply","ProviderName":"SymConnect Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.UltraData.Service.Host","ProviderType":"QuickApply","ProviderName":"UltraData Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.QuickApply.XP.Service.Host","ProviderType":"QuickApply","ProviderName":"XP Quick Apply Provider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.CoA.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositCoAProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.Corelation.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositCorelationProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.DNA.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositDNAProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.Phoenix.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositPhoenixProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.Spectrum.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositSpectrumProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.Static.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositStaticProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositSymitarProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.UltraData.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositUltraDataProvider"},{"PackageId":"Alkami.MicroServices.RDCoreDeposit.XP.Service.Host","ProviderType":"RemoteDepositCoreDeposit","ProviderName":"RemoteDepositCoreDepositXPProvider"},{"PackageId":"Alkami.MicroServices.Registration.Entrust.Service.Host","ProviderType":"Registration","ProviderName":"Alkami Entrust Registration Service"},{"PackageId":"Alkami.MicroServices.Registration.Service.Host","ProviderType":"Registration","ProviderName":"Alkami Registration Service"},{"PackageId":"Alkami.MicroServices.RememberedDevices.Entrust.Service.Host","ProviderType":"RememberedDevices","ProviderName":"Entrust Remembered Devices Provider"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.Dynamic.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceDynamicRDC"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.Ensenta.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceEnsentaRDC"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.Fxd.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceFxdRDC"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.ProfitStars.Onus.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceProfitStarsOnusRDC"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.ProfitStars.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceProfitStarsRDC"},{"PackageId":"Alkami.MicroServices.RemoteDepositProviders.Vertifi.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceVertifiRDC"},{"PackageId":"Alkami.MicroServices.Reporting.BusinessReports.Service.Host","ProviderType":"Reporting","ProviderName":"Business Reports Reporting MicroService"},{"PackageId":"Alkami.MicroServices.Reporting.URA.Service.Host","ProviderType":"Reporting","ProviderName":"User Recent Activity Reporting MicroService"},{"PackageId":"Alkami.MicroServices.Risk.Alkami.Service.Host","ProviderType":"Risk","ProviderName":"Alkami Risk Provider"},{"PackageId":"Alkami.MicroServices.Risk.Biocatch.Service.Host","ProviderType":"Risk","ProviderName":"Biocatch Risk Provider"},{"PackageId":"Alkami.MicroServices.Risk.DetectTA.Service.Host","ProviderType":"Risk","ProviderName":"DetectTA Risk Evaluation"},{"PackageId":"Alkami.MicroServices.Risk.Entrust.Service.Host","ProviderType":"Risk","ProviderName":"Entrust Risk"},{"PackageId":"Alkami.MicroServices.SamlAuth.Service.Host","ProviderType":"SamlAuthentication","ProviderName":"Alkami SamlAuth Authentication Provider"},{"PackageId":"Alkami.MicroServices.SkipPaymentProviders.Corelation.Service.Host","ProviderType":"SkipPayment","ProviderName":"Corelation SkipPayment Provider"},{"PackageId":"Alkami.MicroServices.SkipPaymentProviders.Dynamic.Service.Host","ProviderType":"SkipPayment","ProviderName":"SkipPayment Dynamic Provider"},{"PackageId":"Alkami.MicroServices.SkipPaymentProviders.Patelco.Host","ProviderType":"SkipPayment","ProviderName":"SkipPayment Patelco Provider"},{"PackageId":"Alkami.MicroServices.SkipPaymentProviders.SymConnect.Service.Host","ProviderType":"SkipPayment","ProviderName":"SkipPayment SymConnect Provider"},{"PackageId":"Alkami.MicroServices.SkipPaymentProviders.Veridian.Host","ProviderType":"SkipPayment","ProviderName":"Veridian SkipPayment Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.ACI.VCA.Service.Host","ProviderType":"GenericSSO","ProviderName":"ACI VCA SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.AutoBooks.Service.Host","ProviderType":"GenericSSO","ProviderName":"AutoBooks SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.Avoka.Service.Host","ProviderType":"GenericSSO","ProviderName":"Avoka SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.BALoyaltyRewards.Service.Host","ProviderType":"GenericSSO","ProviderName":"BALoyaltyRewards SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.BillMatrixNext.Service.Host","ProviderType":"GenericSSO","ProviderName":"BillMatrixNext SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.BlackKnightMortgage.Service.Host","ProviderType":"GenericSSO","ProviderName":"Black Knight Mortgage SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.Blend.Service.Host","ProviderType":"GenericSSO","ProviderName":"Blend SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.BlendMortgage.Service.Host","ProviderType":"GenericSSO","ProviderName":"Blend Mortgage SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.CentrixSso.Service.Host","ProviderType":"GenericSSO","ProviderName":"Centrix SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.ClickSwitch.Service.Host","ProviderType":"GenericSSO","ProviderName":"ClickSwitchService"},{"PackageId":"Alkami.MicroServices.SSOProviders.CubusSkipAPay.Service.Host","ProviderType":"GenericSSO","ProviderName":"CubusSkipAPay SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.CubusSso.Service.Host","ProviderType":"GenericSSO","ProviderName":"Cubus Sso Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.CuOneLoanPay.Service.Host","ProviderType":"GenericSSO","ProviderName":"CuOneLoanPay SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.Imsi.Service.Host","ProviderType":"GenericSSO","ProviderName":"ImsiSso"},{"PackageId":"Alkami.MicroServices.SSOProviders.InstantOpen.Service.Host","ProviderType":"GenericSSO","ProviderName":"InstantOpenSsoService"},{"PackageId":"Alkami.MicroServices.SSOProviders.MacuAugeoRewards.Service.Host","ProviderType":"GenericSSO","ProviderName":"Macu AugeoRewards SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.MacuProPay.Service.Host","ProviderType":"GenericSSO","ProviderName":"MacuProPay SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.MagicWrighter.Service.Host","ProviderType":"GenericSSO","ProviderName":"Magic Wrighter SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.MeridianLink.Service.Host","ProviderType":"GenericSSO","ProviderName":"MeridianLink SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.MeridianLinkSsoV2.Service.Host","ProviderType":"GenericSSO","ProviderName":"MeridianLinkSsoV2 Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.MyCardInfo.Service.Host","ProviderType":"GenericSSO","ProviderName":"MyCardInfo SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.OnDot.Service.Host","ProviderType":"GenericSSO","ProviderName":"OnDotMobileGateway"},{"PackageId":"Alkami.MicroServices.SSOProviders.OrionInvestments.Service.Host","ProviderType":"GenericSSO","ProviderName":"Orion Investments SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.PayTraceSso.Service.Host","ProviderType":"GenericSSO","ProviderName":"PayTrace Sso Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.PropayByTSYS.Service.Host","ProviderType":"GenericSSO","ProviderName":"PropayByTSYS SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.QCash.Service.Host","ProviderType":"GenericSSO","ProviderName":"QCash"},{"PackageId":"Alkami.MicroServices.SSOProviders.QuavoDisputeSso.Service.Host","ProviderType":"GenericSSO","ProviderName":"QuavoDispute Sso Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.QuavoStatusSso.Service.Host","ProviderType":"GenericSSO","ProviderName":"QuavoStatus Sso Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.SymApp.Service.Host","ProviderType":"GenericSSO","ProviderName":"SymApp SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.TCILoanOrigination.Service.Host","ProviderType":"GenericSSO","ProviderName":"TCI Loan Origination SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.UChooseRewards.Service.Host","ProviderType":"GenericSSO","ProviderName":"UChooseRewards SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.uOpen.Service.Host","ProviderType":"GenericSSO","ProviderName":"uOpen SSO Provider"},{"PackageId":"Alkami.MicroServices.SSOProviders.VirtualCapture.Service.Host","ProviderType":"GenericSSO","ProviderName":"VirtualCapture SSO Provider"},{"PackageId":"Alkami.MicroServices.StepUp.Alkami.Service.Host","ProviderType":null,"ProviderName":"Alkami Step Up"},{"PackageId":"Alkami.MicroServices.StepUp.Entrust.Service.Host","ProviderType":"StepUp","ProviderName":"Entrust Step Up"},{"PackageId":"Alkami.MicroServices.Transfers.CoreProxy.Service.Host","ProviderType":"Transfers","ProviderName":"Transfers Core Proxy Service"},{"PackageId":"Alkami.MicroServices.UserLookupHandlers.External.Service.Host","ProviderType":"UserLookup","ProviderName":"External User Lookup"},{"PackageId":"Alkami.MicroServices.UserLookupHandlers.Internal.Service.Host","ProviderType":"UserLookup","ProviderName":"Internal User Lookup"},{"PackageId":"Alkami.MicroServices.UUIDGen.SandiaLabs.Service.Host","ProviderType":"UUID","ProviderName":"SandiaLabs ESB UUID Provider"},{"PackageId":"Alkami.MicroServices.UUIDGen.Service.Host","ProviderType":"UUID","ProviderName":"SandiaLabs ESB UUID Provider"},{"PackageId":"Alkami.MicroServices.VisaGiveBack.Service.Host","ProviderType":"visagiveback","ProviderName":"Visa GiveBack Provider"},{"PackageId":"Alkami.MicroServices.WebAnalytics.Adobe.EP.Service.Host","ProviderType":"Web Analytics","ProviderName":"Adobe Analytics Provider"},{"PackageId":"Alkami.MicroServices.WebAnalytics.Google.Service.Host","ProviderType":"Web Analytics","ProviderName":"Google Analytics Provider"},{"PackageId":"Alkami.MicroServices.ZelleProviders.FIS.MACU.Service.Host","ProviderType":"Zelle","ProviderName":"FIS MACU Zelle"},{"PackageId":"Alkami.MicroServices.ZelleProviders.FIS.Service.Host","ProviderType":"Zelle","ProviderName":"FIS Zelle"},{"PackageId":"Alkami.MicroServices.ZelleProviders.FiServ.Service.Host","ProviderType":"Zelle","ProviderName":"FIServ Zelle"},{"PackageId":"Alkami.MicroServices.ZelleProviders.JHA.Service.Host","ProviderType":"Zelle","ProviderName":"JHA Zelle"},{"PackageId":"Alkami.MS.AccountRefresh.SubscriptionFilter.Host","ProviderType":"AccountRefreshSubscriptionFilter","ProviderName":"Alkami MS Account Refresh Subscription Filter"},{"PackageId":"Alkami.MS.ActionableAlertsProviders.Income.Service.Host","ProviderType":"ActionableAlert","ProviderName":"Income Actionable Alert"},{"PackageId":"Alkami.MS.AutomaticDepositAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Automatic Deposit Alert"},{"PackageId":"Alkami.MS.AutomaticWithdrawalAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Automatic Withdrawal Alert"},{"PackageId":"Alkami.MS.BACH.NeedsAuthorizationAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS BACH Needs Authorization Alert"},{"PackageId":"Alkami.MS.BACH.OptInAlerts.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS BACH OptIn Alerts"},{"PackageId":"Alkami.MS.BACH.RejectedByFIAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS BACH RejectedByFI Alert"},{"PackageId":"Alkami.MS.BalanceAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Balance Alert"},{"PackageId":"Alkami.MS.BillingFile.Service.Host","ProviderType":"BillingFile","ProviderName":"Billing File Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.CardApi.Host","ProviderType":"CardManagement","ProviderName":"CardApi Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.CardValet.Host","ProviderType":"CardManagement","ProviderName":"CardValet Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.CodeConnect.Host","ProviderType":"CardManagement","ProviderName":"CodeConnect Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.COOPPortal.Host","ProviderType":"CardManagement","ProviderName":"COOPPortal Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.CoreApi.Host","ProviderType":"CardManagement","ProviderName":"CoreApi Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.Cots.Host","ProviderType":"CardManagement","ProviderName":"COTS Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.CustomControl.Host","ProviderType":"CardManagement","ProviderName":"Custom Control Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.Entrust.Host","ProviderType":"CardManagement","ProviderName":"Entrust Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.FIS.Host","ProviderType":"CardManagement","ProviderName":"FIS Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.JXchange.Host","ProviderType":"CardManagement","ProviderName":"JXchange Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.Phoenix.Host","ProviderType":"CardManagement","ProviderName":"Phoenix Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.PushProvisioning.Host","ProviderType":"CardManagement","ProviderName":"Push Provisioning Card Management Provider"},{"PackageId":"Alkami.MS.CardManagementProviders.Visa.Host","ProviderType":"CardManagement","ProviderName":"Visa Card Management Provider"},{"PackageId":"Alkami.MS.CDMaturity.Spectrum.Host","ProviderType":"CDMaturity","ProviderName":"CDMaturitySpectrumProvider"},{"PackageId":"Alkami.MS.CDMaturity.SymConnect.Host","ProviderType":"CDMaturity","ProviderName":"CDMaturitySymConnectProvider"},{"PackageId":"Alkami.MS.CheckClearedAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Check Cleared Alert"},{"PackageId":"Alkami.MS.CheckOrderProviders.DeluxeSSO.Host","ProviderType":"GenericSSO","ProviderName":"Deluxe CheckOrder SSO Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.CoreApi.Host","ProviderType":"CheckWithdrawal","ProviderName":"CoreApi CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.Corelation.Host","ProviderType":"CheckWithdrawal","ProviderName":"Corelation CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.iPower.Host","ProviderType":"CheckWithdrawal","ProviderName":"iPower CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.OSI.Host","ProviderType":"CheckWithdrawal","ProviderName":"OSI CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.Spectrum.Host","ProviderType":"CheckWithdrawal","ProviderName":"Spectrum CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.Static.Host","ProviderType":"CheckWithdrawal","ProviderName":"Static CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.SymConnect.Host","ProviderType":"CheckWithdrawal","ProviderName":"SymConnect CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.UltraData.Host","ProviderType":"CheckWithdrawal","ProviderName":"UltraData CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.CheckWithdrawal.XP.Host","ProviderType":"CheckWithdrawal","ProviderName":"XP CheckWithdrawal Provider"},{"PackageId":"Alkami.MS.Core.Base2000.Service.Host","ProviderType":"Core","ProviderName":"Base2000CreditCardProvider"},{"PackageId":"Alkami.MS.Core.Corelation.Registration.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MS.Core.Corelation.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MS.Core.DataExchange.Host","ProviderType":"Core","ProviderName":"PSCUDataExchangeCreditCardProvider"},{"PackageId":"Alkami.MS.Core.DMI.Service.Host","ProviderType":"Core","ProviderName":"DMI Core Provider"},{"PackageId":"Alkami.MS.Core.ESB.Salesforce.AccountUpdate.Service.Host","ProviderType":"Core","ProviderName":"Salesforce Core Provider"},{"PackageId":"Alkami.MS.Core.ESB.Salesforce.Registration.Service.Host","ProviderType":"Core","ProviderName":"Salesforce Core Provider"},{"PackageId":"Alkami.MS.Core.ESB.Salesforce.Service.Host","ProviderType":"Core","ProviderName":"Salesforce Core Provider"},{"PackageId":"Alkami.MS.Core.Legacy.Corelation.Registration.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MS.Core.Legacy.Corelation.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MS.Core.Legacy.SymConnect.Registration.Service.Host","ProviderType":"Core","ProviderName":"SymConnectCoreProvider"},{"PackageId":"Alkami.MS.Core.Legacy.SymConnect.Service.Host","ProviderType":"Core","ProviderName":"SymConnectCoreProvider"},{"PackageId":"Alkami.MS.Core.LoanPayoffCalculator.CoreAPI.Service.Host","ProviderType":"Core","ProviderName":"CoreApi Core Provider"},{"PackageId":"Alkami.MS.Core.LoanPayoffCalculator.XP.Service.Host","ProviderType":"Core","ProviderName":"XP Core Provider"},{"PackageId":"Alkami.MS.Core.LPC.Corelation.Service.Host","ProviderType":"Core","ProviderName":"Corelation Core Provider"},{"PackageId":"Alkami.MS.Core.Orion.Force.Service.Host","ProviderType":"Core","ProviderName":"Orion Force Core Provider"},{"PackageId":"Alkami.MS.Core.Orion.Service.Host","ProviderType":"Core","ProviderName":"Orion Core Provider"},{"PackageId":"Alkami.MS.Core.Phoenix.AccountUpdate.Host","ProviderType":"Core","ProviderName":"HarlandPhoenixCore"},{"PackageId":"Alkami.MS.Core.Phoenix.Host","ProviderType":"Core","ProviderName":"HarlandPhoenixCore"},{"PackageId":"Alkami.MS.Core.Phoenix.Registration.Host","ProviderType":"Core","ProviderName":"HarlandPhoenixCore"},{"PackageId":"Alkami.MS.Core.SymConnect.Registration.Service.Host","ProviderType":"Core","ProviderName":"SymConnect Core Provider MS"},{"PackageId":"Alkami.MS.Core.SymConnect.Service.Host","ProviderType":"Core","ProviderName":"SymConnect Core Provider MS"},{"PackageId":"Alkami.MS.Core.TMG.FirstTech.Service.Host","ProviderType":"Core","ProviderName":"First Tech TMG Credit Card Provider"},{"PackageId":"Alkami.MS.Core.TMG.Host","ProviderType":"Core","ProviderName":"TMGCreditCardProvider"},{"PackageId":"Alkami.MS.CorePasswordUpdate.Corelation.Service.Host","ProviderType":"CorePasswordUpdate","ProviderName":"Corelation Core Password Update"},{"PackageId":"Alkami.MS.CourtesyPayProviders.CoreApi.Host","ProviderType":"CourtesyPay","ProviderName":"CoreApi Courtesy Pay Provider"},{"PackageId":"Alkami.MS.CourtesyPayProviders.DirectFCU.Host","ProviderType":"CourtesyPay","ProviderName":"DirectFCU Courtesy Pay Provider"},{"PackageId":"Alkami.MS.CourtesyPayProviders.Patelco.Host","ProviderType":"CourtesyPay","ProviderName":"Patelco Courtesy Pay Provider"},{"PackageId":"Alkami.MS.CourtesyPayProviders.Static.Host","ProviderType":"CourtesyPay","ProviderName":"Static Courtesy Pay Provider"},{"PackageId":"Alkami.MS.CreditScoreInfoProviders.Savvy.Host","ProviderType":"CreditScoreInfo","ProviderName":"SavvyMoneyCreditScoreInfoProvider"},{"PackageId":"Alkami.MS.Crypto.Dynamic.Service.Host","ProviderType":"Crypto","ProviderName":"Alkami Crypto Provider"},{"PackageId":"Alkami.MS.Crypto.Nydig.Service.Host","ProviderType":"Crypto","ProviderName":"Nydig Crypto Provider"},{"PackageId":"Alkami.MS.DebitCardPurchaseAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Debit Card Purchase Alert"},{"PackageId":"Alkami.MS.DirectDepositAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Direct Deposit Alert"},{"PackageId":"Alkami.MS.EM.ACHRiskProcessor.Host","ProviderType":"EventProcessor","ProviderName":"AlkamiACHRiskProcessor"},{"PackageId":"Alkami.MS.EM.BusinessPayee.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Business Payee Alert Processor Service"},{"PackageId":"Alkami.MS.EM.BusinessReportsBackfill.Processor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Business Reports Backfill Processor Service"},{"PackageId":"Alkami.MS.EM.CardValet.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MS EM CardValet Processor"},{"PackageId":"Alkami.MS.EM.CryptoReceiptProcessor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Crypto Receipt Processor Service"},{"PackageId":"Alkami.MS.EM.CryptoReconProcessor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Crypto Reconciliation Processor Service"},{"PackageId":"Alkami.MS.EM.CryptoTransaction.Alert.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MicroServices Event Management Crypto Transaction Processor Service"},{"PackageId":"Alkami.MS.EM.Processor.ExternalBilling.Host","ProviderType":"EventProcessor","ProviderName":"ExternalBillingReportProvider"},{"PackageId":"Alkami.MS.EM.Processor.ScheduledACH.Host","ProviderType":"EventProcessor","ProviderName":"AlkamiScheduledACH"},{"PackageId":"Alkami.MS.EM.Processor.ScheduledWire.Host","ProviderType":"EventProcessor","ProviderName":"AlkamiScheduledWire"},{"PackageId":"Alkami.MS.EM.Scheduler.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management Scheduler Processor Service"},{"PackageId":"Alkami.MS.EM.SubscriptionBasedBilling.Host","ProviderType":"EventProcessor","ProviderName":"SubscriptionBasedBillingProvider"},{"PackageId":"Alkami.MS.EM.UserNotificationProcessor.Host","ProviderType":"EventProcessor","ProviderName":"Alkami MS EM User Notification Processor"},{"PackageId":"Alkami.MS.Fact.CoreAPI.Host","ProviderType":"Fact Provider","ProviderName":"Alkami.MS.Fact.CoreAPI"},{"PackageId":"Alkami.MS.Fact.Symitar.Service.Host","ProviderType":"Fact Provider","ProviderName":"Alkami.MS.Fact.Symitar"},{"PackageId":"Alkami.MS.GenericProxy.Service.Host","ProviderType":"GenericProxy","ProviderName":"Alkami.MS.GenericProxy"},{"PackageId":"Alkami.MS.InsufficientFundsAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Insufficient Funds Alert"},{"PackageId":"Alkami.MS.InvalidPasswordAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Invalid Password Alert"},{"PackageId":"Alkami.MS.Investment.FirstTech.Host","ProviderType":"Investment","ProviderName":"First Tech Investments Provider"},{"PackageId":"Alkami.MS.Investment.Orcas.Host","ProviderType":"Investment","ProviderName":"ORCAS Investments Provider"},{"PackageId":"Alkami.MS.LoanPaymentDueAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Loan Payment Due Alert"},{"PackageId":"Alkami.MS.LocationProviders.COOP.Host","ProviderType":"Location","ProviderName":"AlkamiCOOPLocationProvider"},{"PackageId":"Alkami.MS.LocationProviders.COOP.ProximitySearch.Host","ProviderType":"Location","ProviderName":"AlkamiProximitySearchLocationProvider"},{"PackageId":"Alkami.MS.LocationProviders.Dynamic.Host","ProviderType":"Location","ProviderName":"AlkamiDynamicLocationProvider"},{"PackageId":"Alkami.MS.LocationProviders.LocatorSearch.Host","ProviderType":"Location","ProviderName":"AlkamiLocatorSearchLocationProvider"},{"PackageId":"Alkami.MS.LocationProviders.MoneyPass.Host","ProviderType":"Location","ProviderName":"AlkamiMoneyPassLocationProvider"},{"PackageId":"Alkami.MS.LocationProviders.Patelco.Host","ProviderType":"Location","ProviderName":"AlkamiLocationPatelcoProvider"},{"PackageId":"Alkami.MS.MM.ExternalAccount.Alert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS MM External Account Alert"},{"PackageId":"Alkami.MS.MM.MultiAccount.Alert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS MM Multi Account Alert"},{"PackageId":"Alkami.MS.Notifications.SMS.Clickatell.Service.Host","ProviderType":"SMS","ProviderName":"Alkami MS Notifications SMS Clickatell"},{"PackageId":"Alkami.MS.Notifications.SMS.Telesign.Service.Host","ProviderType":"SMS","ProviderName":"Alkami MicroServices Notifications SMS Telesign"},{"PackageId":"Alkami.MS.Notifications.SMS.Twilio.Service.Host","ProviderType":"SMS","ProviderName":"Alkami MS Notifications SMS Twilio"},{"PackageId":"Alkami.MS.NotificationWidget.Examples.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Notification Widget Example Alert"},{"PackageId":"Alkami.MS.ODPP.CoreApi.Host","ProviderType":"OverdraftProtection","ProviderName":"CoreApi Overdraft Protection Provider"},{"PackageId":"Alkami.MS.ODPP.Static.Host","ProviderType":"OverdraftProtection","ProviderName":"Static Overdraft Service"},{"PackageId":"Alkami.MS.ODPP.XP.Host","ProviderType":"OverdraftProtection","ProviderName":"XP Overdraft Protection Provider"},{"PackageId":"Alkami.MS.Processor.MessageCenter.SLA.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management SLA Notification Processor Service"},{"PackageId":"Alkami.MS.Processor.MessageCenter.SLA.Service.Host","ProviderType":"EventProcessor","ProviderName":"Alkami Event Management SLA Notification Processor Service"},{"PackageId":"Alkami.MS.Push.UrbanAirship.Service.Host","ProviderType":"Push","ProviderName":"Alkami MS Push UrbanAirship"},{"PackageId":"Alkami.MS.QuickApply.Corelation.Service.Host","ProviderType":"QuickApply","ProviderName":"Alkami.MS.QuickApply.Corelation Quick Apply Provider"},{"PackageId":"Alkami.MS.QuickApply.Silverlake.Service.Host","ProviderType":"QuickApply","ProviderName":"Alkami.MS.QuickApply.Silverlake Quick Apply Provider"},{"PackageId":"Alkami.MS.RDC.Dynamic.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceDynamicRDC"},{"PackageId":"Alkami.MS.RDC.Ensenta.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceEnsentaRDC"},{"PackageId":"Alkami.MS.RDC.Fxd.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceFxdRDC"},{"PackageId":"Alkami.MS.RDC.ProfitStars.Onus.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceProfitStarsOnusRDC"},{"PackageId":"Alkami.MS.RDC.ProfitStars.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceProfitStarsRDC"},{"PackageId":"Alkami.MS.RDC.Vertifi.Service.Host","ProviderType":"RemoteDeposit","ProviderName":"MicroserviceVertifiRDC"},{"PackageId":"Alkami.MS.ReturnedCheckAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Returned Check Alert"},{"PackageId":"Alkami.MS.SkipPaymentProviders.CoreApi.Service.Host","ProviderType":"SkipPayment","ProviderName":"CoreApi SkipPayment Provider"},{"PackageId":"Alkami.MS.SkipPaymentProviders.Spectrum.Host","ProviderType":"SkipPayment","ProviderName":"SkipPayment Spectrum Provider"},{"PackageId":"Alkami.MS.SkipPaymentProviders.XP.Host","ProviderType":"SkipPayment","ProviderName":"XP SkipPayment Provider"},{"PackageId":"Alkami.MS.SSOProviders.AugeoRewards.Host","ProviderType":"GenericSSO","ProviderName":"Augeo Rewards Generic SSO Provider"},{"PackageId":"Alkami.MS.SSOProviders.AvtexPureConnect.Service.Host","ProviderType":"GenericSSO","ProviderName":"Avtex Pure Connect SSO"},{"PackageId":"Alkami.MS.SSOProviders.CUDirectSSO.Service.Host","ProviderType":"GenericSSO","ProviderName":"CU Direct SSO Provider"},{"PackageId":"Alkami.MS.SSOProviders.CUDL.Service.Host","ProviderType":"GenericSSO","ProviderName":"CUDL SSO Provider"},{"PackageId":"Alkami.MS.SSOProviders.LoanRateReset.Service.Host","ProviderType":"GenericSSO","ProviderName":"LoanRateReset"},{"PackageId":"Alkami.MS.SSOProviders.VirtualStrongBox.Service.Host","ProviderType":"GenericSSO","ProviderName":"VirtualStrongBox SSO Provider"},{"PackageId":"Alkami.MS.StopPayment.CoA.Host","ProviderType":"StopPayParser","ProviderName":"CoA StopPay Provider"},{"PackageId":"Alkami.MS.StopPayment.CoreApi.Host","ProviderType":"StopPayParser","ProviderName":"CoreApi Stop Payment Provider"},{"PackageId":"Alkami.MS.StopPayment.Corelation.Host","ProviderType":"StopPayParser","ProviderName":"Corelation Stop Payment Provider"},{"PackageId":"Alkami.MS.StopPayment.Dynamic.Host","ProviderType":"StopPayParser","ProviderName":"DynamicStopPayInputProvider"},{"PackageId":"Alkami.MS.StopPayment.iPower.Host","ProviderType":"StopPayParser","ProviderName":"iPower StopPay Provider"},{"PackageId":"Alkami.MS.StopPayment.Miser.Host","ProviderType":"StopPayParser","ProviderName":"MiserStopPayInputProvider"},{"PackageId":"Alkami.MS.StopPayment.OSI.Host","ProviderType":"StopPayParser","ProviderName":"OSIStopPayInputProvider"},{"PackageId":"Alkami.MS.StopPayment.Phoenix.Host","ProviderType":"StopPayParser","ProviderName":"HarlandPhoenixStopPayProvider"},{"PackageId":"Alkami.MS.StopPayment.Silverlake.Host","ProviderType":"StopPayParser","ProviderName":"Silverlake StopPay Provider"},{"PackageId":"Alkami.MS.StopPayment.Spectrum.Host","ProviderType":"StopPayParser","ProviderName":"Spectrum Stop Payment Provider"},{"PackageId":"Alkami.MS.StopPayment.SymConnect.Host","ProviderType":"StopPayParser","ProviderName":"SymConnect Stop Pay Input Provider"},{"PackageId":"Alkami.MS.StopPayment.UltraData.Host","ProviderType":"StopPayParser","ProviderName":"UltraData StopPay Provider"},{"PackageId":"Alkami.MS.StopPayment.XP.Host","ProviderType":"StopPayParser","ProviderName":"XP StopPay Provider"},{"PackageId":"Alkami.MS.SyncCoordinator.AccountSync.Host","ProviderType":"Syncing","ProviderName":"Alkami MS SyncCoordinator AccountSync"},{"PackageId":"Alkami.MS.SyncCoordinator.Host","ProviderType":"Syncing","ProviderName":"Alkami MS Sync Coordinator"},{"PackageId":"Alkami.MS.TransactionAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Transaction Alert"},{"PackageId":"Alkami.MS.TransactionDescriptionAlert.Host","ProviderType":"AlertProcessor","ProviderName":"Alkami MS Transaction Description Alert"},{"PackageId":"Alkami.MS.TransactionExportProviders.BAI.Host","ProviderType":"TransactionExports","ProviderName":"AlkamiBAIExportProvider"},{"PackageId":"Alkami.MS.TransactionExportProviders.CSV.Host","ProviderType":"TransactionExports","ProviderName":"AlkamiCSVExportProvider"},{"PackageId":"Alkami.MS.TransactionExportProviders.OFX.Host","ProviderType":"TransactionExports","ProviderName":"AlkamiOFXExportProvider"},{"PackageId":"Alkami.MS.TransactionExportProviders.QBO.Host","ProviderType":"TransactionExports","ProviderName":"AlkamiQBOExportProvider"},{"PackageId":"Alkami.MS.TransactionExportProviders.QFX.Host","ProviderType":"TransactionExports","ProviderName":"AlkamiQFXExportProvider"},{"PackageId":"Alkami.MS.TransactionImage.Catalyst.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami MS TransactionImage Catalyst"},{"PackageId":"Alkami.MS.TransactionImage.CorporateAmerica.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami MS TransactionImage CorporateAmerica"},{"PackageId":"Alkami.MS.TransactionImage.FedImage.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami MS TransactionImage Fed Image"},{"PackageId":"Alkami.MS.TransactionImage.FinastraActiveView.Service.Host","ProviderType":"TransactionImage","ProviderName":"Finastra ActiveView TransactionImage Provider MS"},{"PackageId":"Alkami.MS.TransactionImage.FiservDirector.Service.Host","ProviderType":"TransactionImage","ProviderName":"Fiserv Director TransactionImage Provider MS"},{"PackageId":"Alkami.MS.TransactionImage.JHA4sight.Service.Host","ProviderType":"TransactionImage","ProviderName":"JHA4sight TransactionImage Provider MS"},{"PackageId":"Alkami.MS.TransactionImage.MidAtlantic.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami.MS.TransactionImage.MidAtlantic"},{"PackageId":"Alkami.MS.TransactionImage.MVI.Service.Host","ProviderType":"TransactionImage","ProviderName":"MVI TransactionImage ProviderMS"},{"PackageId":"Alkami.MS.TransactionImage.SFSolutions.Service.Host","ProviderType":"TransactionImage","ProviderName":"SFSolutions TransactionImage ProviderMS"},{"PackageId":"Alkami.MS.TransactionImage.StandardSSO.Service.Host","ProviderType":"TransactionImage","ProviderName":"Standard SSO Transaction Image Provider MS"},{"PackageId":"Alkami.MS.TransactionImage.Static.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami.MS.TransactionImage.Static"},{"PackageId":"Alkami.MS.TransactionImage.Synergy.Service.Host","ProviderType":"TransactionImage","ProviderName":"Alkami.MS.TransactionImage.Synergy"},{"PackageId":"Alkami.MS.WireProcessor.DirectLine.Service.Host","ProviderType":"WireProcessor","ProviderName":"DirectLine"},{"PackageId":"Alkami.MS.WireProcessor.Static.Service.Host","ProviderType":"WireProcessor","ProviderName":"Static"},{"PackageId":"Alkami.MS.WireProcessor.WireXChange.Service.Host","ProviderType":"WireProcessor","ProviderName":"WireXChange"},{"PackageId":"fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host","ProviderType":"Core","ProviderName":"SymConnectCoreProvider"}]} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.Tests.retired.ps1 b/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.Tests.retired.ps1 new file mode 100644 index 0000000..a2c82d4 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.Tests.retired.ps1 @@ -0,0 +1,129 @@ +# Load the Dependant Modules From Disk +if (Get-Module -Name Alkami.DevOps.Common) { + Remove-Module -Name Alkami.DevOps.Common +} + +$commonModulePath = Get-ChildItem -Filter 'Alkami.DevOps.Common.psd1' -Recurse -ErrorAction Continue +Write-Host ("Forcing load of Alkami.DevOps.Common from {0}" -f $commonModulePath.FullName) +Import-Module $commonModulePath.FullName -Force + +## TODO: There is no more Alkami.DevOps.Operations module ... +# Load the Module Under Test From Disk +$deployModulePath = Get-ChildItem -Filter 'Alkami.DevOps.Operations.psd1' -Recurse -ErrorAction Continue +Write-Host ("Forcing load of Alkami.DevOps.Operations from {0}" -f $deployModulePath.FullName) +if (Get-Module -Name Alkami.DevOps.Operations) { + Remove-Module -Name Alkami.DevOps.Operations -Force} + +Import-Module $deployModulePath.FullName -Force + + +InModuleScope Alkami.DevOps.Operations { + Describe "Copy-AlkamiReleaseManifest" { + + # Temp folder to do tests in. + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Host "Writing test files to $tempPath" + + # Make Directories/Files for Tests + + $testFiles = @{ + # Current Deploy + "Current\test.txt" = "test" + "Current\DirA\a.txt" = "a" + "Current\DirA\b.txt" = "b" + "Current\DirA\c.txt" = "oldValue" + + "NewRelease\DirA\a.txt" = "a" + "NewRelease\DirA\b.txt" = "b" + "NewRelease\DirA\c.txt" = "newValue" + "NewRelease\DirB\d.txt" = "d" + "NewRelease\DirB\e.txt" = "e" + + "NewerRelease\DirA\a.txt" = "a" + "NewerRelease\DirA\b.txt" = "1" + "NewerRelease\DirA\c.txt" = "c" + "NewerRelease\DirB\d.txt" = "2" + } + + foreach($key in $testFiles.keys) + { + $file = (Join-Path $tempPath $key) + $content = $testFiles[$key] + + New-Item -ItemType File -Path $file -Force | Out-Null + Set-Content -Force -Path $file -Value $content + } + + # Do the first copy from "NewRelease" into "Current" + $src = (Join-Path $tempPath "NewRelease") + $dst = (Join-Path $tempPath "Current") + Copy-AlkamiReleaseManifest $src $dst -DiffMove -Verbose + + It "Deletes a file that was not in the newest release." { + $deletedFile = (Join-Path $dst "test.txt") + Write-Host "Checking that $deletedFile does not exist.." + Test-Path $deletedFile | Should Be $false + } + + It "Creates new folder and files in the release." { + $newFile1 = (Join-Path $dst "DirB\d.txt") + $newFile2 = (Join-Path $dst "DirB\e.txt") + + Test-Path $newFile1 | Should Be $true + Get-Content $newFile1 | Should Match "d" + Test-Path $newFile2 | Should Be $true + Get-Content $newFile2 | Should Match "e" + } + + It "Updated a changed file." { + $changedFile = (Join-Path $dst "DirA\c.txt") + Test-Path $changedFile | Should Be $true + Get-Content $changedFile | Should Match "newValue" + } + + It "Created manifest files." { + $manifest = (Join-Path $dst "manifest.txt") + $manifestLeftovers = (Join-Path $dst "manifestLeftovers.txt") + + Test-Path $manifest | Should Be $true + Test-Path $manifestLeftovers | Should Be $true + } + + # Copy in an extra file to make sure it gets preserved through the next copy. + $extraFile = (Join-Path $dst "DirB\extraFile.txt") + New-Item -ItemType File -Path $extraFile -Force | Out-Null + Set-Content -Force -Path $extraFile -Value "things and stuff!" + + # Do the next copy from "NewerRelease" into "Current" + $src = (Join-Path $tempPath "NewerRelease") + $dst = (Join-Path $tempPath "Current") + Copy-AlkamiReleaseManifest $src $dst -DiffMove -Verbose + + It "Does not delete the extra file from outside the release." { + Test-Path $extraFile | Should Be $true + Get-Content $extraFile | Should Match "things and stuff!" + } + + It "Updated the further-changed files." { + + foreach($key in $testFiles.keys) + { + if($key -like "NewerRelease") + { + $file = (Join-Path $dst $key) + $content = $testFiles[$key] + + Get-Content -Path $file | Should Match $content + } + } + } + + It "Deleted file removed from release." { + + $deletedFile = (Join-Path $dst "DirB\e.txt") + Test-Path $deletedFile | Should Be $false + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.ps1 b/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.ps1 new file mode 100644 index 0000000..a208f12 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Retired/Copy-AlkamiReleaseManifest.ps1 @@ -0,0 +1,214 @@ +function Copy-AlkamiReleaseManifest { +<# +.SYNOPSIS + Copies Orb with a file manifest to track files between releases. + Files that are not tracked by the manifest are left alone. + DiffMove flag makes it so that unchanged files are not copied. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [Alias("TempOrbPath")] + [string]$srcPath = "C:\temp\deploy\orb", + + [Parameter(Mandatory=$false)] + [Alias("OrbPath")] + [string]$dstPath, + + [Parameter(Mandatory=$false)] + [switch] $diffMove + ) + + $logLead = (Get-LogLeadName); + + if([string]::IsNullOrEmpty($dstPath)) { + $dstPath = (Get-OrbPath) + } + + if($dstPath -eq $srcPath) + { + Write-Warning "$logLead Cannot copy Alkami release from/to the same folder. $srcPath" + return + } + + $timer = [System.Diagnostics.Stopwatch]::StartNew() + + $manifestPath = Join-Path $dstPath "manifest.txt" + $manifestLeftoverPath = Join-Path $dstPath "manifestLeftovers.txt" + + $exclude = "*log4net.config" + + Write-Host "$logLead Discovering files to copy:" + Write-Host "$logLead Note: log4net.config files are ignored!" + Write-Host "$loglead Discovering files in $srcPath" + $srcFiles = (Get-RelativeFileList $srcPath -exclude $exclude) + + $dstFiles = @(); + if(Test-Path $manifestPath) + { + Write-Host "$loglead Loading file paths from manifest file: $manifestPath" + $dstFiles = (Get-Content $manifestPath) + } + else + { + Write-Host "$logLead Release manifest not detected at $manifestPath" + Write-Host "$logLead Discovering files in $dstPath" + $dstFiles = (Get-RelativeFileList $dstPath -exclude $exclude) + } + + Write-Host "`n$logLead Copying Alkami release from $srcPath to $dstPath" + Write-Host "$logLead Determining files to add." + $filesToAdd = (Get-SetDifference $srcFiles $dstFiles) + Write-Host "$logLead Determining files to delete." + $filesToDelete = (Get-SetDifference $dstFiles $srcFiles) + Write-Host "$logLead Determining files to move." + $filesToMove = @(); + $filesToMove = (Get-SetDifference $srcFiles $filesToAdd); + $filesToMove = (Get-SetDifference $filesToMove $filesToDelete) + + if($diffMove.isPresent) + { + Write-Host "$logLead DiffMove flag is present. Finding files that have not changed to avoid copies." + + # Only move the files that have changed by hash comparison. + $newFilesToMove = @(); + foreach($file in $filesToMove) + { + $srcFile = Join-Path $srcPath $file + $dstFile = Join-Path $dstPath $file + if((Get-FileHash -path $srcFile).hash -ne (Get-FileHash -path $dstFile).hash) + { + Write-Verbose "$logLead $srcFile hash is different than $dstFile hash." + $newFilesToMove += $file + } + } + + # Reassign $filesToMove to a list of fewer files to copy. + $count = $filesToMove.Count + $filesToMove = $newFilesToMove + $count = $count - $filesToMove.Count + Write-Host "$logLead $count files don't need to be updated!" + } + + $addCount = $filesToAdd.count; + $delCount = $filesToDelete.count; + $movCount = $filesToMove.count; + + Write-Host "`n$logLead Peforming the following actions:" + Write-Host "$logLead Adding $addCount new files." + Write-Host "$logLead Copying $movCount files." + Write-Host "$logLead Deleting $delCount existing files.`n" + + $filesToCopy = $filesToAdd + $filesToMove + if($filesToCopy.Count) + { + Write-Host "$logLead Starting file copies." + + $filesPerJob = 3000; + $maxJobs = 8; + $numFiles = $filesToCopy.Count + $numJobs = [Math]::Ceiling($numFiles / $filesPerJob); + Write-Host "NumJobs $numJobs" + + $scriptBlock = { + + param ($srcPath, $dstPath, $copyFiles) + + $VerbosePreference = 'Continue' + + foreach($file in $copyFiles) + { + $srcFile = Join-Path $srcPath $file + $dstFile = Join-Path $dstPath $file + + if(!(Test-Path $dstFile)) + { + Write-Verbose "$logLead File $dstFile doesn't exist. Creating." + New-Item -ItemType File -Path $dstFile -Force | Out-Null + } + + Write-Verbose "$logLead Copying $file" + Copy-Item $srcFile $dstFile -Force + } + } + + $jobs = @() + for($i = 0; $i -lt $numJobs; $i++) + { + $start = $i * $filesPerJob + $end = ($i + 1) * $filesPerJob + + if($start -ge $numFiles) + { + break; + } + elseif($end -gt ($numFiles - 1)) + { + $end = ($numFiles - 1) + } + + $copyFiles = $filesToCopy[$start..$end] + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $srcPath, $dstPath, $copyFiles + + $running = @($jobs | Where-Object {$_.State -in ('Running','NotStarted')}) + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) + { + $finished = Wait-Job -Job $jobs -Any + $running = @($jobs | Where-Object {$_.State -in ('Running','NotStarted')}) + } + } + + # Wait for all jobs in this session to complete. + Get-Job | Wait-Job > $null + + # Receive-Job to output the logs. + $jobs | ForEach-Object { $_ | Receive-Job } + } + else + { + Write-Host "$logLead No files to copy." + } + + if($delCount) + { + Write-Host "$logLead Starting file deletes." + foreach($file in $filesToDelete) + { + $dstFile = Join-Path $dstPath $file + if(test-path $dstFile) + { + Write-Verbose "$logLead Deleting $dstFile" + Remove-Item -Path $dstFile -Force + } + } + } + else + { + Write-Host "$logLead No files to delete." + } + + Write-Host "`n$logLead Writing file manifest to $manifestPath" + Set-Content -Force -Path $manifestPath -Value $srcFiles + + # Write out leftover files that survived the release. + Write-Host "`n$logLead Now determining leftover files that were untouched by the release copy in $dstPath" + $dstFiles = (Get-RelativeFileList $dstPath) + $leftoverFiles = Get-SetDifference $dstFiles $srcFiles + + Write-Host "$logLead Writing leftover files to $manifestLeftoverPath" + Set-Content -Force -Path $manifestLeftoverPath -Value $leftoverFiles + + $symlinkPath = (Join-Path $dstPath "createSymlinks.ps1") + if (Test-Path $symlinkPath) + { + Write-Output "$logLead Running Symlink script." + & $symlinkPath + } + else + { + Write-Warning ("$logLead Symlink script not found at `"$symlinkPath`"") + } + + $timer.Stop(); + Write-Host ("`n$logLead Alkami Manifest Release Copy finished in [{0}]" -f $timer.Elapsed.ToString()) +} diff --git a/Modules/Alkami.DevOps.Operations/Retired/Get-RelativeFileList.ps1 b/Modules/Alkami.DevOps.Operations/Retired/Get-RelativeFileList.ps1 new file mode 100644 index 0000000..e29b467 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/Retired/Get-RelativeFileList.ps1 @@ -0,0 +1,29 @@ +function Get-RelativeFileList { +<# +.SYNOPSIS + Returns a list of file names minus the base path. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$path = ".\", + [Parameter(Mandatory=$false)] + [string]$exclude = "" + ) + + $files = (Get-FilesNoSymlink $path) + + # Trim the base path off. + $results = @() + $basePath = (Get-Item -path $path).FullName + $basePathSubstringLength = ($basePath.length + 1) # +1 gets leading slash. + $files | ForEach-Object { + if($_ -notlike $exclude) + { + $results += $_.substring($basePathSubstringLength) + } + } + + return $results +} + diff --git a/Modules/Alkami.DevOps.Operations/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Operations/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..e915b5b --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/tools/chocolateyInstall.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + $resourcesFolder = (Join-Path -Path $parentPath -ChildPath "Resources") + if (Test-Path -Path $resourcesFolder) { + + Write-Host "Copying resources folder for module $id to [$targetModuleVersionPath]" + Copy-Item -Path $resourcesFolder -Destination $targetModuleVersionPath -Recurse -Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Operations/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Operations/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.Operations/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.nuspec b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.nuspec new file mode 100644 index 0000000..f7cafef --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.nuspec @@ -0,0 +1,31 @@ + + + + Alkami.DevOps.SqlReports + $version$ + Alkami Platform Modules - DevOps - SqlReports + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.SqlReports + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the DevOps SqlReports module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.psd1 b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.psd1 new file mode 100644 index 0000000..c7669b8 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.psd1 @@ -0,0 +1,18 @@ +@{ + RootModule = 'Alkami.DevOps.SqlReports.psm1' + ModuleVersion = '3.20.6' + GUID = '889e054f-4f72-4a9a-8140-b3150bbe6e1c' + Author = 'SRE, cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'Cmdlets used to manage reports for the ORB report server' + RequiredModules = 'Alkami.PowerShell.Common', 'Alkami.PowerShell.Configuration', 'Alkami.DevOps.Common' + FunctionsToExport = 'Add-ORBReportDataSources','Get-PublishedDataSources','Get-PublishedSSRSReports','Get-ReportUserCredentialsFromSecretServer','Get-SSRSDatabaseServer','Get-SSRSReportHashes','Grant-RightsToSSRSFolder','New-ExecutionProxy','New-ReportServerDataSource','New-ReportServerFolder','New-SSRSProxy','New-SSRSReportsFolder','Publish-SqlReports','Publish-SSRSChangedReports','Publish-SSRSReport','Publish-SSRSReportsDirectory','Remove-Datasources','Set-ReportDataSource','Set-ReportParameters' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'ssrs', 'reports') + ProjectUri = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Operations+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } +} diff --git a/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.pssproj b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.pssproj new file mode 100644 index 0000000..7799a64 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Alkami.DevOps.SqlReports.pssproj @@ -0,0 +1,69 @@ + + + + Debug + 2.0 + {0f8e0fc4-8e30-4e2f-ac91-7377fe38879f} + Exe + MyApplication + MyApplication + Alkami.DevOps.SqlReports + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.SqlReports") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/AlkamiManifest.xml b/Modules/Alkami.DevOps.SqlReports/AlkamiManifest.xml new file mode 100644 index 0000000..f89a7a0 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.SqlReports + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.SqlReports/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.SqlReports/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..d503b77 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Private/VariableDeclarations.ps1 @@ -0,0 +1,6 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +$Global:SSRSProxy = $null; +$Global:SSRSExecutionProxy = $null; +$DisposeSessions = $true; diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Add-ORBReportDataSources.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Add-ORBReportDataSources.ps1 new file mode 100644 index 0000000..f213614 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Add-ORBReportDataSources.ps1 @@ -0,0 +1,168 @@ +function Add-ORBReportDataSources { + +<# +.SYNOPSIS + Creates the Necessary ORB DataSources +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [Alias("ConnectionString")] + [string]$masterConnectionString, + + [Parameter(Mandatory=$false)] + [Alias("ReportServerUrl")] + [string]$reportServerEndpoint, + + [Parameter(Mandatory=$false)] + [Alias("FolderName")] + [string]$parentFolder, + + [Parameter(Mandatory=$false)] + [Alias("ReportServerUserName")] + [string]$reportsUser, + + [Parameter(Mandatory=$false)] + [Alias("ReportServerPassword")] + [string]$reportsPassword, + + [Parameter(Mandatory=$false)] + [Alias("Force")] + [switch]$forceOverwriteDataSources, + + [Parameter(Mandatory=$false)] + [string]$environmentType + ) + + $logLead = (Get-LogLeadName); + + if (!(Test-IsWebServer) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : This function can only be automatically executed on a web tier server. To run, call the function with the appropriate parameters." + return + } + + try + { + [xml]$config = Get-ReportServerConfiguration -WarningAction SilentlyContinue + + if ($null -ne $config) + { + $reportServerEndpointNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServer""]/@value") + $reportFolderNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerPath""]/@value") + $reportUserNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerUserName""]/@value") + $reportPasswordNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerPassword""]/@value") + $reportEnvironmentType = $config.appSettings.SelectSingleNode("//add[@key=""Environment.Type""]/@value") + } + + if ((($null -eq $reportServerEndpointNode) -or ([String]::IsNullOrEmpty($reportServerEndpointNode.Value))) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServer"" appSetting from the machine.config and no report server URL was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportFolderNode) -or ([String]::IsNullOrEmpty($reportFolderNode.Value))) -and [String]::IsNullOrEmpty($parentFolder)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPath"" appSetting from the machine.config and no parent folder name was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportUserNode) -or ([String]::IsNullOrEmpty($reportUserNode.Value))) -and [String]::IsNullOrEmpty($reportsUser)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerUserName"" appSetting from the machine.config and no user name was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportPasswordNode) -or ([String]::IsNullOrEmpty($reportPasswordNode.Value))) -and [String]::IsNullOrEmpty($reportsPassword)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPassword"" appSetting from the machine.config and no password was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportEnvironmentType) -or ([String]::IsNullOrEmpty($reportEnvironmentType.Value))) -and [String]::IsNullOrEmpty($environmentType)) + { + Write-Warning "$logLead : Could not read the value for the ""Environment.Type"" appSetting from the machine.config and value was provided as a parameter. Execution cannot continue." + return; + } + + $proxyUrlToUse = IsNull $reportServerEndpoint $reportServerEndpointNode.Value + $proxy = New-SSRSProxy $proxyUrlToUse + + $parentFolderToUse = IsNull $parentFolder $reportFolderNode.Value + $usernameToUse = IsNull $reportsUser $reportUserNode.Value + $passwordToUse = IsNull $reportsPassword $reportPasswordNode.Value + + $normalizedParentFolder = ("/" + $parentFolderToUse.TrimStart("/").TrimEnd("/")) + + # Create the AlkamiMaster Data Source, points to the master database + $adminDatasourceCheck = $proxy.GetItemType($normalizedParentFolder + "/AlkamiMaster") + if ($null -eq $adminDatasourceCheck -or $adminDatasourceCheck -eq "Unknown" -or $forceOverwriteDataSources.IsPresent) + { + Write-Host ("$logLead : Creating AlkamiMaster DataSource") + (New-ReportServerDataSource $proxy "AlkamiMaster" $masterConnectionString $usernameToUse $passwordToUse $normalizedParentFolder -forceOverwriteDataSources:$forceOverwriteDataSources) | Out-Null + } + else + { + Write-Warning ("$logLead : The AlkamiMaster DataSource Already Exists in Folder {0}. To overwrite pass the -Force parameter" -f $normalizedParentFolder) + } + + # Create the Alkami Data Source, the connection string is set by the application + $clientDatasourceCheck = $proxy.GetItemType($normalizedParentFolder + "/Alkami") + if ($null -eq $clientDatasourceCheck -or $clientDatasourceCheck -eq "Unknown" -or $forceOverwriteDataSources.IsPresent) + { + Write-Host ("$logLead : Creating Alkami DataSource") + (New-ReportServerDataSource $proxy "Alkami" "PlaceHolder" $usernameToUse $passwordToUse $normalizedParentFolder -forceOverwriteDataSources:$forceOverwriteDataSources) | Out-Null + } + else + { + Write-Warning ("$logLead : The Alkami DataSource Already Exists in Folder {0}. To overwrite pass the -Force parameter" -f $normalizedParentFolder) + } + + # Create the Alkami_EDW Data Source, points to Flux. + $fluxDatasourceCheck = $proxy.GetItemType($normalizedParentFolder + "/Alkami_EDW") + if ($null -eq $fluxDatasourceCheck -or $fluxDatasourceCheck -eq "Unknown" -or $forceOverwriteDataSources.IsPresent) + { + Write-Host ("$logLead : Creating Alkami_EDW DataSource") + + #If $reportEnvironmentType wasn't found in machine.config(or this is an agent), use the param + $reportEnvironmentType = IsNull $reportEnvironmentType.Value $environmentType + + # Determine the Flux connection string. + $fluxConnectionStrings = @{ + Production = "Data source=FLUXEDW-1;Initial Catalog=Alkami_EDW"; + Staging = "Data Source=RC-EDW1;Initial Catalog=Alkami_EDW"; + QA = "Data Source=QA-FLUXDB1;Initial Catalog=Alkami_EDW"; + Development = "Data Source=DEV-FLUXDB1;Initial Catalog=Alkami_EDW"; + } + + $fluxConnectionString = $fluxConnectionStrings[$reportEnvironmentType] + + #Create the datasource + if (!([String]::IsNullOrEmpty($fluxConnectionString))) { + (New-ReportServerDataSource $proxy "Alkami_EDW" $fluxConnectionString $usernameToUse $passwordToUse $normalizedParentFolder -forceOverwriteDataSources:$forceOverwriteDataSources) | Out-Null + } else { + Write-Warning("$logLead : Flux EDW connection string could not be determined based on Environment.Type: {$reportEnvironmentType} and was not configured.") + } + } + else + { + Write-Warning ("$logLead : The Alkami_EDW DataSource Already Exists in Folder {0}. To overwrite pass the -Force parameter" -f $normalizedParentFolder) + } + } + finally + { + if ($DisposeSessions) + { + if ($null -ne $SSRSProxy) + { + $SSRSProxy.Dispose() + } + + if ($null -ne $SSRSExecutionProxy) + { + $SSRSExecutionProxy.Dispose() + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedDataSources.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedDataSources.ps1 new file mode 100644 index 0000000..f3bf265 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedDataSources.ps1 @@ -0,0 +1,35 @@ +function Get-PublishedDataSources { + <# +.SYNOPSIS + Get and return the data sources from a SOAP webservice, a SQL Reports Server + +.PARAMETER Proxy + A SOAP proxy for the SQL Reports Server + +.PARAMETER Folder + The reports folder to look for Data Sources in + +#> + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$Proxy, + + [Parameter(Position = 1, Mandatory = $true)] + [Alias("reportfolder")] + [string]$Folder + ) + $logLead = Get-LogLeadName + + if ($folder[0] -ne "/") { + Write-Verbose ("$logLead : Transform {0} => /{0}" -f $folder) + $folder = "/" + $folder + } + + Write-Verbose ("$logLead : Querying children of {0}" -f $folder) + $datasources = $proxy.ListChildren($folder, $false) | Where-Object {$_.TypeName -eq "DataSource"} + + Write-Verbose ("$logLead : {0} datasource objects located." -f $datasources.Count) + return $datasources +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedSSRSReports.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedSSRSReports.ps1 new file mode 100644 index 0000000..558af40 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Get-PublishedSSRSReports.ps1 @@ -0,0 +1,35 @@ +function Get-PublishedSSRSReports { + <# +.SYNOPSIS + Get the list of Published SSRS reports from a SQL Reports Server + +.PARAMETER Proxy + The SOAP client proxy used to query the SQL Reports Server + +.PARAMETER Folder + The Reports Folder on the SQL Reports Server to query for published reports +#> + + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$Proxy, + + [Parameter(Position = 1, Mandatory = $true)] + [Alias("reportfolder")] + [string]$Folder + ) + $logLead = Get-LogLeadName + + if ($Folder[0] -ne "/") { + Write-Verbose ("$logLead : Transform {0} => /{0}" -f $Folder) + $Folder = "/" + $Folder + } + + Write-Verbose ("$logLead : Querying children of {0}" -f $Folder) + $reports = $Proxy.ListChildren($Folder, $false) | Where-Object { $_.TypeName -eq "Report" } + + Write-Verbose ("$logLead : {0} report objects located." -f $reports.Count) + return $reports +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Get-ReportUserCredentialsFromSecretServer.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Get-ReportUserCredentialsFromSecretServer.ps1 new file mode 100644 index 0000000..33ddc47 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Get-ReportUserCredentialsFromSecretServer.ps1 @@ -0,0 +1,75 @@ +function Get-ReportUserCredentialsFromSecretServer () { +<# +.SYNOPSIS + Gets the username and password of a report user secret from Secret Server. + +.PARAMETER secretUserName + Username of the user to authenticate with on Secret Server. + +.PARAMETER secretPassword + Password of the user to authenticate with on Secret Server. + +.PARAMETER environmentName + The environment name of the report user to retrieve (e.g. "12") + +.PARAMETER environmentType + The environment type of the report user to retrieve (e.g. "Production") + +.OUTPUTS + Either an object containing the username and password of the reports user or null. + +.EXAMPLE + Get-ReportUserCredentialsFromSecretServer -secretUserName "BobBarker" -secretPassword "PIR123!" -environmentName "12" -environmentType "Production" + +Password Username +-------- -------- +ExamplePwd ExampleUser +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [String]$secretUserName, + [Parameter(Mandatory=$true)] + [String]$secretPassword, + [Parameter(Mandatory=$true)] + [String]$environmentName, + [Parameter(Mandatory=$true)] + [String]$environmentType + ) + + $loglead = (Get-LogLeadName) + + # Note: If the bootstrap scripts are modified to pass in a credential object, this won't be necessary. + $secretCredential = New-Object System.Management.Automation.PSCredential ( $secretUserName , (Get-SecureString $secretPassword)) + + $folderName = "ReportUsers" + $result = $null + + # Determine the name of the secret based on environment type. + # Only production (for now) has separate secrets per-environment. + $secretName = $null + if($environmentType -eq "Production") { + # Extract the major pod from the name. + $dotSearch = $environmentName.IndexOf(".") + if($dotSearch -ge 0) { + $environmentName = $environmentName.Substring(0, $dotSearch) + } + $secretName = "$environmentType-$environmentName-ReportUser" + } else { + $secretName = "$environmentType-ReportUser" + } + + Write-Verbose "$loglead : Searching for secret '$secretName' in folder '$folderName'" + $resultCredential = ( Get-UserCredentialsFromSecretServer $secretCredential $folderName $secretName ) + + # Note: If the bootstrap scripts are modified to accept credential results, this won't be necessary. + if ( $null -ne $resultCredential ) { + + $result = New-Object PSObject -Property @{ + 'Username' = $resultCredential.UserName + 'Password' = (Get-PasswordFromCredential $resultCredential) + } + } + + return $result +} diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSDatabaseServer.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSDatabaseServer.ps1 new file mode 100644 index 0000000..f49ab8f --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSDatabaseServer.ps1 @@ -0,0 +1,31 @@ +function Get-SSRSDatabaseServer { + <# + .SYNOPSIS + Retrieves the configured ReportServer name from an SSRS Server. + + .DESCRIPTION + Generally, this is the same name as the server that is queried, but this adds an additional layer of checking. + + .PARAMETER ReportServer + [string] The server name to be queried. Required. + + .EXAMPLE + Get-SSRSDatabaseServer "sc00rs01.fh.local" + + .EXAMPLE + Get-SSRSDatabaseServer -ReportServer "sc00rs01.fh.local" + + .NOTES + This requires the calling user to have sysadmin rights to the SSRS server +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Alias("ReportServer")] + [string]$server + ) + $proxy = New-SSRSProxy "http://$server/ReportServer" + $configInfo = [xml]$proxy.GetReportServerConfigInfo($false) + + return $configInfo.ServerConfigInfo.Server.MachineName +} diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSReportHashes.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSReportHashes.ps1 new file mode 100644 index 0000000..2b5a316 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Get-SSRSReportHashes.ps1 @@ -0,0 +1,85 @@ +function Get-SSRSReportHashes { +<# +.SYNOPSIS + Retrieves a hashtable containing the Name of a report and a hashed value genreated from the Report's content. + +.DESCRIPTION + Use this command to generate a hashtable of report hashes for use in determining if a report has changed. + +.PARAMETER SqlReportServer + [string] The machine name of the Reports SQL Server. Required. + +.PARAMETER ReportPath + [string] The case-sensitive ReportPath used in SQL Server (i.e. /AWS Staging QAstg). Required. + +.EXAMPLE + Get-SSRSReportHashes "sc00rs01.fh.local" "/AWS Staging QAstg" + +.EXAMPLE + Get-SSRSReportHashes -SqlReportServer "sc00rs01.fh.local" -ReportPath "/AWS Staging QAstg" +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $true)] + [Alias("SqlReportServer")] + [string]$reportServer, + + [Parameter(Mandatory = $true)] + [Alias("ReportPath")] + [string]$path + ) + + Write-Verbose "$logLead : Filtering data using report path $path" + $connectionString = "data source=$($reportServer);Integrated Security=SSPI;Database=ReportServer;MultiSubnetFailover=true" + Write-Host "$logLead : Connecting to ReportServer Database with connection string: $connectionString" + + $query = @" +SELECT Name, CONVERT(varchar(max), CONVERT(varbinary(max), Content)) as TextContent +FROM dbo.Catalog c +WHERE Content IS NOT NULL +AND PATH like '%$path/%' +ORDER BY Name +"@ + + try { + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($connectionString) + } catch { + Write-Error "$logLead : Exception occurred building the ConnectionString: $($_.Exception.Message.ToString())" + throw $_ + } + + $conn = New-Object System.Data.SqlClient.SqlConnection $conStrBuilder.ToString() + $command = New-Object System.Data.SqlClient.SqlCommand($query, $conn) + $dbReportHashes = @{ } + + try { + if ($conn.State -ne [System.Data.ConnectionState]::Open) { + $conn.Open() + } + $results = $command.ExecuteReader() + + while ($results.Read()) { + $name = $results.Item(0) + $content = $results.Item(1) + + $hashedContent = Get-UTF8ContentHash $content + Write-Verbose "$logLead : Name: $($name) Hash: $($hashedContent)" + + $dbReportHashes.Add($name, $hashedContent) + } + } catch { + Write-Error "$logLead : Exception occurred reading database results: $($_.Exception.Message.ToString())" + throw $_ + } finally { + if ($null -ne $results) { + $results.Dispose() + } + + if ($null -ne $results) { + $conn.Dispose() + } + } + + return $dbReportHashes +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Grant-RightsToSSRSFolder.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Grant-RightsToSSRSFolder.ps1 new file mode 100644 index 0000000..9e0dd1c --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Grant-RightsToSSRSFolder.ps1 @@ -0,0 +1,149 @@ +function Grant-RightsToSSRSFolder { +<# +.SYNOPSIS + Grants Rights to a Folder on the SSRS Server +#> + + [CmdletBinding()] + Param( + [Parameter(Position=0,Mandatory=$true)] + [Alias("Roles")] + [string[]]$roleNames, + + [Parameter(Position=1,Mandatory=$false)] + [Alias("User")] + [string]$userName, + + [Parameter(Position=2,Mandatory=$false)] + [Alias("Folder")] + [string]$folderName, + + [Parameter(Position=3,Mandatory=$false)] + [Alias("ReportServerUrl")] + [string]$reportServerEndpoint + ) + + $logLead = (Get-LogLeadName); + + try + { + if (!(Test-IsWebServer) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : This function can only be automatically executed on a web tier server. To run, call the function with the appropriate parameters." + return + } + + [xml]$config = Get-ReportServerConfiguration -WarningAction SilentlyContinue + + if ($null -ne $config) + { + $reportServerEndpointNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServer""]/@value") + $reportFolderNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerPath""]/@value") + $reportUserNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerUserName""]/@value") + } + + if ((($null -eq $reportServerEndpointNode) -or ([String]::IsNullOrEmpty($reportServerEndpointNode.Value))) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServer"" appSetting from the machine.config and no report server URL was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportFolderNode) -or ([String]::IsNullOrEmpty($reportFolderNode.Value))) -and [String]::IsNullOrEmpty($folderName)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPath"" appSetting from the machine.config and no folder name was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportUserNode) -or ([String]::IsNullOrEmpty($reportUserNode.Value))) -and [String]::IsNullOrEmpty($userName)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerUserName"" appSetting from the machine.config and no user name was provided as a parameter. Execution cannot continue." + return; + } + + $proxyUrlToUse = IsNull $reportServerEndpoint $reportServerEndpointNode.Value + $proxy = New-SSRSProxy $proxyUrlToUse + $proxyNameSpace = $proxy.GetType().Namespace + + $folderToUse = IsNull $folderName $reportFolderNode.Value + $userToUse = IsNull $userName $reportUserNode.Value + + $normalizedFolder = "/" + $folderToUse.TrimStart("/") + + # Make sure the folder exists + try + { + $folderType = $proxy.GetItemType($normalizedFolder) + } + catch + { + Write-Warning ("$logLead : The folder {0} does not exist. Execution cannot continue" -f $normalizedFolder) + return + } + + ## TODO: cbrand ~ candidate for [string]::IsNullOrEmpty() ? + if (($null -eq $folderType) -or ($folderType -eq "Unknown")) + { + Write-Warning ("$logLead : The folder {0} does not exist. Execution cannot continue" -f $normalizedFolder) + return + } + + $folderPolicies = $proxy.GetPolicies($normalizedFolder, [ref]$false) + $userPolicies = $folderPolicies | Where-Object {$_.GroupUserName -eq $userToUse} + + if ($null -eq $userPolicies) + { + # Add a Policy for the User + Write-Output ("$logLead : Creating User Policy") + + $policy = New-Object "${proxyNameSpace}.Policy" + $policy.GroupUserName = $userToUse + $folderPolicies += $policy + + [array]$userPolicies += $policy + } + + # Add the Role to the User Policy + Write-Output ("$logLead : Creating User Role") + + $rolesDirty = $false; + foreach ($roleToAdd in $roleNames) + { + if ($userPolicies | Where-Object {$_.Roles.Name -eq $roleToAdd}) + { + Write-Output ("$logLead : User {0} already has role {1} on folder {2}" -f $userToUse, $roleToAdd, $folderToUse) + continue; + } + + $role = New-Object "${proxyNameSpace}.Role" + $role.Name = $roleToAdd + ($userPolicies | Select-Object -First 1).Roles += $role + Write-Output ("$logLead : User {0} granted role {1} on folder {2}" -f $userToUse, $roleToAdd, $folderToUse) + $rolesDirty = $true + } + + if ($rolesDirty) + { + $proxy.SetPolicies($normalizedFolder, $folderPolicies) + Write-Output "$logLead : Role Changes Committed" + } + else + { + Write-Output "$logLead : No Role Changes Required" + } + } + finally + { + if ($null -ne $SSRSProxy) + { + $SSRSProxy.Dispose() + } + + if ($null -ne $SSRSExecutionProxy) + { + $SSRSExecutionProxy.Dispose() + } + } +} + +#region Private Functions + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/New-ExecutionProxy.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/New-ExecutionProxy.ps1 new file mode 100644 index 0000000..f6b2a01 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/New-ExecutionProxy.ps1 @@ -0,0 +1,40 @@ +function New-ExecutionProxy { + <# +.SYNOPSIS + Create a SSRS(ReportExecution2005.asmx) Execution Proxy. If a $Global:SSRSExecutionProxy already exists, return that. Otherwise, create it, set it, and return it. + +.PARAMETER WebServiceUrl + Url of the SSRS webservice +#> + + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [Alias("url")] + [string]$WebServiceUrl + ) + + $logLead = Get-LogLeadName + + if ($null -ne $Global:SSRSExecutionProxy) { + Write-Host "$logLead : Using Existing Global Execution Proxy" + return $Global:SSRSExecutionProxy + } + + # Add trailing / + if ($WebServiceUrl -notcontains "ASMX") { + if (!($WebServiceUrl.EndsWith("/"))) { + Write-Verbose ("$logLead : Transform {0} => {0}/" -f $WebServiceUrl) + $WebServiceUrl = $WebServiceUrl + "/" + } + + Write-Verbose ("$logLead : Transform {0} => {0}ReportExecution2010.asmx" -f $WebServiceUrl) + $WebServiceUrl = $WebServiceUrl + "ReportExecution2005.asmx" + } + + Write-Host ("$logLead : Creating new Execution Web Service Proxy for Url {0}" -f $WebServiceUrl) + $Global:ExecutionProxy = New-WebServiceProxy -Uri $WebServiceUrl -UseDefaultCredential -ErrorAction 0 + + return $Global:ExecutionProxy +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerDataSource.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerDataSource.ps1 new file mode 100644 index 0000000..8769b45 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerDataSource.ps1 @@ -0,0 +1,62 @@ +function New-ReportServerDataSource { +<# +.SYNOPSIS + Creates a new DataSource Using the Supplied Parameters +#> + [CmdletBinding()] + param ( + [Parameter(Position=0,Mandatory=$true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$proxy, + + [Parameter(Position=1,Mandatory=$true)] + [string]$DataSourceName, + + [Parameter(Position=2,Mandatory=$true)] + [string]$ConnectionString, + + [Parameter(Position=3,Mandatory=$true)] + [string]$UserName, + + [Parameter(Position=4,Mandatory=$true)] + [string]$Password, + + [Parameter(Position=5,Mandatory=$true)] + [string]$ParentFolder, + + [Parameter(Mandatory=$false)] + [Alias("Force")] + [switch]$ForceOverwriteDataSources + ) + + $proxyNameSpace = $proxy.GetType().Namespace + $dataSource = New-Object("$proxyNameSpace.DataSourceDefinition") + + $dataSource.ConnectString = $ConnectionString + $dataSource.Extension = "SQL" + $dataSource.Enabled = $true + + $dataSource.CredentialRetrieval = [SSRS.CredentialRetrievalEnum]::Store + $dataSource.ImpersonateUser = $false + $dataSource.ImpersonateUserSpecified = $true + $dataSource.WindowsCredentials = $true + $dataSource.UserName = $UserName + $dataSource.Password = $Password + + $propertyHash = @{ + 'ConnectString' = $ConnectionString; + 'UserName' = $UserName; + 'Password' = $Password; + 'WindowsCredentials' = $true; + 'Enabled' = $true; + 'Extension' = "SQL"; + 'ImpersonateUser' = $false; + 'ImpersonateUserSpecified' = $true; + 'CredentialRetrieval' = [SSRS.CredentialRetrievalEnum]::Store; + } + + $propertyCollection = $propertyHash.Keys.foreach{ @{ Name = $_; Value = $propertyHash[$_] } -as "${proxyNameSpace}.property" } + $newDataSource = $proxy.CreateDataSource($DataSourceName, $ParentFolder, $ForceOverwriteDataSources.IsPresent, $dataSource, $propertyCollection) + + return $newDataSource +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerFolder.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerFolder.ps1 new file mode 100644 index 0000000..32d222c --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/New-ReportServerFolder.ps1 @@ -0,0 +1,55 @@ +function New-ReportServerFolder { + <# +.SYNOPSIS + Create a new report folder on the SSRS server + +.PARAMETER Proxy + SSRS Proxy + +.PARAMETER ReportFolder + Folder to create + +.PARAMETER ReportPath + Path in which to create ReportFolder +#> + + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$Proxy, + + [Parameter(Position = 1, Mandatory = $true)] + [string]$ReportFolder, + + [Parameter(Position = 2, Mandatory = $false)] + [string]$ReportPath = "/" + ) + + $logLead = Get-LogLeadName + Write-Verbose ("$logLead : Checking for existing folder {0}." -f $ReportFolder) + # Check if folder exists, create if not found + + try { + $folderType = $Proxy.GetItemType("/" + $ReportFolder.TrimStart("/")) + } catch { + Write-Output ("$logLead : Folder {0} does not exist, it will be created" -f $ReportFolder) + } + + if ($null -eq $folderType -or $folderType -eq "Unknown") { + try { + $Proxy.CreateFolder($ReportFolder, $ReportPath, $null) | Out-Null + Write-Output ("$logLead : Created new folder {0}/{1}" -f $ReportPath.TrimEnd("/"), $ReportFolder) + } catch [System.Web.Services.Protocols.SoapException] { + if ($_.Exception.Detail.InnerText -like "*rsItemAlreadyExists*") { + Write-Verbose ("$logLead : Folder {0}/{1} already exists." -f $ReportPath, $ReportFolder) + } else { + $msg = ("$logLead : Error creating folder: {0}/{1}. Msg: '{2}'" -f $ReportPath, $ReportFolder, $_.Exception.Detail.InnerText) + Write-Error $msg + } + } catch { + Write-Warning ("$logLead : An Unknown Error Occurred -- {0}" -f $_.Exception.Message) + } + } else { + Write-Output ("$logLead : Folder {0}/{1} already exists" -f $ReportPath.TrimEnd("/"), $ReportFolder) + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSProxy.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSProxy.ps1 new file mode 100644 index 0000000..9678918 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSProxy.ps1 @@ -0,0 +1,39 @@ +function New-SSRSProxy { + <# +.SYNOPSIS + Create a SSRS(ReportService2010.asmx) Service Proxy. If a $Global:SSRSProxy already exists, return that. Otherwise, create it, set it, and return it. + +.PARAMETER WebServiceUrl + Url of the SSRS webservice +#> + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [Alias("url")] + [string]$WebServiceUrl + ) + + $logLead = Get-LogLeadName + + if ($null -ne $Global:SSRSProxy) { + Write-Host "$logLead : Using Existing Global Proxy" + return $Global:SSRSProxy + } + + # Add trailing / + if ($WebServiceUrl -notcontains "ASMX") { + if (!($WebServiceUrl.EndsWith("/"))) { + Write-Verbose ("$logLead : Transform {0} => {0}/" -f $WebServiceUrl) + $WebServiceUrl = $WebServiceUrl + "/" + } + + Write-Verbose ("$logLead : Transform {0} => {0}ReportService2010.asmx" -f $WebServiceUrl) + $WebServiceUrl = $WebServiceUrl + "ReportService2010.asmx" + } + + Write-Host ("$logLead : Creating new Web Service Proxy for Url {0}" -f $WebServiceUrl) + $Global:SSRSProxy = New-WebServiceProxy -Uri $WebServiceUrl -UseDefaultCredential -ErrorAction 0 -Namespace SSRS + + return $Global:SSRSProxy +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSReportsFolder.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSReportsFolder.ps1 new file mode 100644 index 0000000..3f8f984 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/New-SSRSReportsFolder.ps1 @@ -0,0 +1,98 @@ +function New-SSRSReportsFolder { +<# +.SYNOPSIS +Creates a New Top Level Folder in SSRS + +.DESCRIPTION +Creates a folder with the specified name in the root directory of the specified SSRS server. If no parameters are provided, and +the function is being run on a Web server, the function attempts to read the configured ReportServer and ReportServerPath settings +from the machine.config + +.PARAMETER folderName +[string] The name of the folder to create + +.PARAMETER reportServerEndpoint +[string] The URL to the SSRS Report Server + +.INPUTS +None + +.OUTPUTS +None + +.EXAMPLE +C:\Users\dsage> New-SSRSReportsFolder +[New-ReportServerFolder] : Created new folder /Firehost Production 999 + +.EXAMPLE +PS C:\Users\dsage> New-SSRSReportsFolder -FolderName "Firehost Production 999" -ReportServerUrl "http://ALKA02VMR001/ReportServer" +[New-ReportServerFolder] : Created new folder /Firehost Production 999 +#> + + [CmdletBinding()] + Param( + [Parameter(Position=0,Mandatory=$false)] + [Alias("Name")] + [string]$folderName, + + [Parameter(Position=1,Mandatory=$false)] + [Alias("ReportServerUrl")] + [string]$reportServerEndpoint + ) + + $logLead = (Get-LogLeadName); + + try + { + if (!(Test-IsWebServer) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : This function can only be automatically executed on a web tier server. To run, call the function with the appropriate parameters." + return + } + + [xml]$config = Get-ReportServerConfiguration -WarningAction SilentlyContinue + + if ($null -ne $config) + { + $reportServerEndpointNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServer""]/@value") + $reportFolderNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerPath""]/@value") + } + + if ((($null -eq $reportServerEndpointNode) -or ([String]::IsNullOrEmpty($reportServerEndpointNode.Value))) -and [String]::IsNullOrEmpty($reportServerEndpoint)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServer"" appSetting from the machine.config and no report server URL was provided as a parameter. Execution cannot continue." + return; + } + + if ((($null -eq $reportFolderNode) -or ([String]::IsNullOrEmpty($reportFolderNode.Value))) -and [String]::IsNullOrEmpty($folderName)) + { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPath"" appSetting from the machine.config and no folder name was provided as a parameter. Execution cannot continue." + return; + } + + $folderToUse = IsNull $folderName $reportFolderNode.Value.TrimStart("/") + Write-Verbose ("$logLead : Using Report Folder $folderToUse") + + $proxyUrlToUse = IsNull $reportServerEndpoint $reportServerEndpointNode.Value + Write-Verbose ("$logLead : Using Proxy Endpoint $proxyUrlToUse") + + $proxy = New-SSRSProxy $proxyUrlToUse + New-ReportServerFolder $proxy $folderToUse + } + finally + { + if ($DisposeSessions) + { + if ($null -ne $SSRSProxy) + { + $SSRSProxy.Dispose() + } + + if ($null -ne $SSRSExecutionProxy) + { + $SSRSExecutionProxy.Dispose() + } + } + } +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSChangedReports.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSChangedReports.ps1 new file mode 100644 index 0000000..87e98b1 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSChangedReports.ps1 @@ -0,0 +1,173 @@ +function Publish-SSRSChangedReports { + <# + .SYNOPSIS + Publishes any changed SSRS report files to a report server + + .DESCRIPTION + Loads and compares all SSRS report files in the provided folder against the reports stored on the report database server. + + .PARAMETER ReportServiceUrl + [string] The url of the report server endpoint. Required. + + .PARAMETER ReportFolder + [string] The case-sensitive ReportPath used in SQL Server (i.e. /AWS Staging QAstg). Required. + + .PARAMETER ReportsDirectory + [string] The full folder location containing all report .rdl files to be compared. Required. + + .PARAMETER ServerUserName + [string] The username used to publish to the reports server. Required. + + .PARAMETER ServerPassword + [string] The password used to publish to the reports server. Required. + + .PARAMETER MaximumJobs + [int] The maximum number of job threads to be run in parallel. Not Required. Default: 5. + + .PARAMETER WhatIf + [switch] If the WhatIf switch is provided, no actual changes will be made against the SQL server. Use this for performing test runs. + + .EXAMPLE + Publish-SSRSChangedReports "http://SC00RS01.fh.local/ReportServer" "/AWS Staging QAstg" "C:\Temp\Deploy\AdminReports" "reportuser" "PASSWORD" -WhatIf + + .EXAMPLE + Publish-SSRSChangedReports -ReportServiceUrl "http://SC00RS01.fh.local/ReportServer" -ReportFolder "/AWS Staging QAstg" -ReportsDirectory "D:\Temp\Deploy\AdminReports" -WhatIf -Verbose + + #> + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [Alias("ReportServiceUrl")] + [string]$serviceUrl, + + [Parameter(Mandatory = $true)] + [Alias("ReportFolder")] + [string]$folder, + + [Parameter(Mandatory = $true)] + [Alias("ReportsDirectory")] + [string]$directory, + + [Parameter(Mandatory = $true)] + [Alias("ServerUserName")] + [string]$reportServerUserName, + + [Parameter(Mandatory = $true)] + [Alias("ServerPassword")] + [string]$reportServerPassword, + + [Parameter(Mandatory = $false)] + [Alias("MaximumJobs")] + [int]$maxJobs = 5 + ) + + $logLead = (Get-LogLeadName) + + if (-not (Test-Path -Path $directory)) { + Write-Host "$logLead : Folder not found at [$directory], nothing to do here" + return + } + + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + #Extract the report server name from the service URL. + $reportServer = ([uri]$serviceUrl).Host + + #Get the Name and Hash of all the Enviroment's reports in one call + Write-Verbose ("$logLead : Getting Reports Content from '$reportServer' for '$folder'") + $dbReportHashes = Get-SSRSReportHashes $reportServer $folder + + Write-Verbose ("$logLead : {0} reports located in DB." -f $dbReportHashes.Count) + + #Get and loop through the files + Write-Verbose ("$logLead : Getting *.rdl from {0}" -f $directory) + $reportFiles = Get-ChildItem $directory -Recurse -Include *.rdl + Write-Verbose ("$logLead : {0} report definition files located." -f $reportFiles.Count) + + if(Test-IsCollectionNullOrEmpty $reportFiles) + { + Write-Host "$logLead : No report files found. Exiting early." + return + } + + $updateCount = 0 + $jobs = @() + + $scriptBlock = { + param ($reportFile, $webServiceUrl, $reportFolder, $reportServerUserName, $reportServerPassword) + + Write-Verbose ("$logLead : Preparing RDL {0}" -f $reportFile.FullName) + + [xml]$reportXml = Get-Content $reportFile + $description = $reportXml.Report.Description + + if ([String]::IsNullOrEmpty($description)) { + $description = [String]::Empty + } elseif ($description -match "-") { + $description = $description.Split("-")[0] + } + + Write-Verbose ("$logLead : Report description set to {0}" -f $description) + + (Publish-SSRSReport -webServiceUrl $webServiceUrl ` + -rdlPath $reportFile.FullName ` + -reportFolder $reportFolder ` + -avoidDoubleHop $True ` + -reportServerUserName $reportServerUserName ` + -reportServerPassword $reportServerPassword) ` + | Out-Null +} + +foreach ($reportFile in $reportFiles) { + Write-Verbose ("$logLead : Beginning hash comparison for $($reportFile.Name)") + $reportFileKey = $reportFile.Name.Split(".")[0] + $reportFileHash = Get-FileContentHash $reportFile.FullName + + #Lookup the DB Hash and do work accordingly + $foundDbHash = $dbReportHashes[$reportFileKey] + + if (($null -ne $foundDbHash) -and ($foundDbHash -eq $reportFileHash)) { + Write-Host ("$logLead : Hash hasn't changed for report {0}. Not re-publishing." -f $reportFile.Name) + } else { + Write-Host ("$logLead : Starting Load Job for {0}." -f $reportFile.Name) + Write-Verbose ("$logLead : File and Db hashes to not match. `nFile: $reportFileHash `nDB: $foundDbHash") + + if ($PSCmdlet.ShouldProcess($reportFile.Name, "Publish SSRS Report File")) { + + Write-Verbose ("$logLead : Adding Job for Publish-SSRSReport") + Write-Verbose ("$logLead : -webServiceUrl $serviceUrl") + Write-Verbose ("$logLead : -rdlPath $($reportFile.FullName)") + Write-Verbose ("$logLead : -reportFolder $folder") + Write-Verbose ("$logLead : -reportServerUserName $reportServerUserName") + + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $reportFile, $serviceUrl, $folder, $reportServerUserName, $reportServerPassword + + $running = @($jobs | Where-Object { $_.State -in ('Running', 'NotStarted') }) + + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) { + Wait-Job -Job $jobs -Any | Out-Null + $running = @($jobs | Where-Object { $_.State -in ('Running', 'NotStarted') }) + } + } + + $updateCount++ + $foundDbHash = $Null + } +} + +try { + if ($jobs) { + # Receive-Job to output the logs + Receive-Job -Job $jobs -Wait -AutoRemoveJob + } +} +catch { + Write-Host "$logLead : Exception occurred while receiving jobs. Carry on." + Write-Host "$logLead : Exception was: $_" +} + +$functionStopWatch.Stop() +Write-Host ("$logLead : Total SSRS Report Files Analyzed: $($reportFiles.Count)") +Write-Host ("$logLead : Total SSRS Reports Updated: $updateCount") +Write-Host ("$logLead : Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString()) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReport.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReport.ps1 new file mode 100644 index 0000000..6beb621 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReport.ps1 @@ -0,0 +1,100 @@ +function Publish-SSRSReport { +<# +.SYNOPSIS + Publish SSRS report to SSRS server + +.PARAMETER WebServiceUrl + Url of SSRS webservice + +.PARAMETER RdlPath + Path to report file + +.PARAMETER ReportFolder + Folder on SSRS server to publish report to + +.PARAMETER AvoidDoubleHop + Parameter to pass to Set-ReportDataSource to avoid DoubleHob issue + +.PARAMETER ReportServerUserName + Username for SSRS server + +.PARAMETER ReportServerPassword + Password for SSRS server + +#> + [CmdletBinding()] + Param( + [Parameter(Position=0, Mandatory=$true)] + [Alias("url")] + [string]$WebServiceUrl, + + [ValidateScript({Test-Path $_})] + [Parameter(Position=1, Mandatory=$true)] + [Alias("rdl")] + [string]$RdlPath, + + [Parameter(Position=2, Mandatory=$true)] + [Alias("folder")] + [string]$ReportFolder, + + [Parameter(Position=3, Mandatory=$true)] + [Alias("ApplyDoubleHopFix")] + [bool]$AvoidDoubleHop, + + [Parameter(Position=4, Mandatory=$false)] + [Alias("Username")] + [string]$ReportServerUserName, + + [Parameter(Position=5, Mandatory=$false)] + [Alias("Password")] + [string]$ReportServerPassword + ) + + $logLead = Get-LogLeadName + + # Create Proxy + Write-Verbose "$logLead : Calling New-SSRSProxy" + $proxy = New-SSRSProxy $WebServiceUrl + New-ExecutionProxy -WebServiceUrl $WebServiceUrl | Out-Null + + # Set reportname if blank, default will be the filename without extension + $reportName = [System.IO.Path]::GetFileNameWithoutExtension($RdlPath) + Write-Verbose ("$logLead : Report name set to: {0}" -f $reportName) + + #Get Report content in bytes + Write-Host ("$logLead : Publishing report file {0}" -f $reportName) + Write-Verbose "$logLead : Getting file content (byte[]) of : $RdlPath" + $byteArray = Get-Content $RdlPath -encoding byte + Write-Verbose ("$logLead : Total length: {0}" -f $byteArray.Length) + + if (!($ReportFolder.StartsWith("/"))) { + Write-Verbose ("$logLead : Transform {0} => /{0}" -f $ReportFolder) + $ReportFolder = "/" + $ReportFolder + } + + Write-Verbose ("$logLead : Uploading to: {0}" -f $ReportFolder) + + #Call Proxy to upload report + Write-Host "$logLead : --- Uploading RDL" + [Ref]$UploadWarnings = $null + $proxy.CreateCatalogItem("Report", $reportName, $ReportFolder, $true, $byteArray, $null, $UploadWarnings) | Out-Null + + Write-Verbose "$logLead : Calling Get-PublishedSSRSReports" + $allItems = Get-PublishedSSRSReports $proxy $ReportFolder + $publishedReport = $allItems | Where-Object {($_.Name -eq $reportName)} | Sort-Object ModifiedDate -Descending | Select-Object -first 1 + + if ($null -ne $publishedReport) { + Write-Host "$logLead : --- Setting DataSources" + Write-Verbose "$logLead : Calling Set-ReportDataSource" + Write-Verbose ("Username: $ReportServerUserName") + Set-ReportDataSource $proxy $publishedReport.Path $RdlPath $AvoidDoubleHop -Username $ReportServerUserName -Password $ReportServerPassword + + Write-Host "$logLead : --- Setting Parameters" + Write-Verbose "$logLead : Calling Set-ReportParameters" + Set-ReportParameters $proxy $executionProxy $publishedReport.Path $RdlPath + } else { + Write-Warning ("$logLead : Could not locate published report {0}. Verify the upload and datasources manually." -f $reportName) + } + + Write-Host "$logLead : --- Done" +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.Tests.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.Tests.ps1 new file mode 100644 index 0000000..6f50437 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.Tests.ps1 @@ -0,0 +1,117 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +describe 'Publish-SSRSReportsDirectory' { + + $reportDirectory = $env:TEMP + $reportFolder = "foo report" + $reportHashLocation = "someHashLocation" + + $webServiceUrl = "http:\\foo.com" + + mock -ModuleName $moduleForMock Get-ChildItem -MockWith { + [PSCustomObject] ` + @{ Name = '.\someReport.rdl' } + } + + mock -ModuleName $moduleForMock Get-Content -MockWith { + [xml] + " + + + SQL + Data Source=JunkSource;Initial Catalog=JunkCatalog + true + + 00000000-0000-0000-0000-000000000000 + + Fizz-Buzz + + " + } + + mock -ModuleName $moduleForMock Import-Csv -MockWith { + [PSCustomObject] ` + @{ + Algorithm = "SHA256" + Hash = "I'm a matching hash" + Path = ".\someReport.rdl" + } + } + + mock -ModuleName $moduleForMock Publish-SSRSReport -MockWith { + 123 + } + + mock -ModuleName $moduleForMock Start-Job -MockWith { + + Invoke-Command -ScriptBlock {'Do Nothing'} -ComputerName localhost -AsJob + } + + mock -ModuleName $moduleForMock Wait-Job -MockWith {$null} + + it "should not throw when reportHashLocation is null" { + + mock -ModuleName $moduleForMock Get-FileHash -MockWith { + [PSCustomObject] ` + @{ + Algorithm = "SHA256" + Hash = "I'm a matching hash" + Path = ".\someReport.rdl" + } + } + + { Publish-SSRSReportsDirectory $reportDirectory $webServiceUrl $reportFolder -avoidDoubleHop $true -Verbose } ` + | should -Not -Throw + } + + it "should call Start-Job when a provided reportHashLocation is null" { + + mock -ModuleName $moduleForMock Get-FileHash -MockWith { + [PSCustomObject] ` + @{ + Algorithm = "SHA256" + Hash = "I'm a matching hash" + Path = ".\someReport.rdl" + } + } + + Publish-SSRSReportsDirectory $reportDirectory $webServiceUrl $reportFolder -avoidDoubleHop $true -Verbose + Assert-MockCalled -ModuleName $moduleForMock Start-Job -Scope It + } + + it "should call start-job when a provided hash doesn't match" { + + mock -ModuleName $moduleForMock Get-FileHash -mockwith { + [PSCustomObject] ` + @{ + Algorithm = "SHA256" + Hash = "I'm a mismatched hash" + Path = ".\someReport.rdl" + } + } + + Publish-SSRSReportsDirectory $reportdirectory $webserviceurl $reportfolder -avoiddoublehop $true -filehashlocation $reportHashLocation -verbose + Assert-MockCalled -ModuleName $moduleForMock Start-Job -scope it + } + + it "should not call start-job when a provided hash matches" { + + mock -ModuleName $moduleForMock Get-FileHash -mockwith { + [PSCustomObject] ` + @{ + Algorithm = "SHA256" + Hash = "I'm a matching hash" + Path = ".\someReport.rdl" + } + } + + Publish-SSRSReportsDirectory $reportdirectory $webserviceurl $reportfolder -avoiddoublehop $true -filehashlocation $reportHashLocation -verbose + Assert-MockCalled -ModuleName $moduleForMock Start-Job -exactly 0 -scope it + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.ps1 new file mode 100644 index 0000000..483ec07 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SSRSReportsDirectory.ps1 @@ -0,0 +1,121 @@ +function Publish-SSRSReportsDirectory { + <# +.SYNOPSIS + +.PARAMETER ReportDirectory +.PARAMETER WebServiceUrl +.PARAMETER ReportFolder +.PARAMETER AvoidDoubleHop +.PARAMETER ReportServerUserName +.PARAMETER ReportServerPassword +.PARAMETER ReportHashLocation + +#> + + [CmdletBinding()] + Param( + [ValidateScript( { Test-Path $_ })] + [Parameter(Position = 0, Mandatory = $true)] + [string]$ReportDirectory, + + [Parameter(Position = 1, Mandatory = $true)] + [Alias("url")] + [string]$WebServiceUrl, + + [Parameter(Position = 2, Mandatory = $true)] + [string]$ReportFolder, + + [Parameter(Position = 3, Mandatory = $true)] + [Alias("ApplyDoubleHopFix")] + [bool]$AvoidDoubleHop, + + [Parameter(Position = 4, Mandatory = $false)] + [Alias("Username")] + [string]$ReportServerUserName, + + [Parameter(Position = 5, Mandatory = $false)] + [Alias("Password")] + [string]$ReportServerPassword, + + [Parameter(Position = 6, Mandatory = $false)] + [Alias("FileHashLocation")] + $ReportHashLocation = $null + ) + + $logLead = Get-LogLeadName + + if (-not (Test-Path -Path $ReportDirectory)) { + Write-Host "$logLead : Folder not found at [$ReportDirectory], nothing to do here" + return + } + + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + Write-Verbose ("$logLead : Getting *.rdl from {0}" -f $ReportDirectory) + $reportFiles = Get-ChildItem $ReportDirectory -Recurse -Include *.rdl + Write-Verbose ("$logLead : {0} report definition files located." -f $reportFiles.Count) + + $maxJobs = 5 + $jobs = @() + + $scriptBlock = { + + param ($reportFile, $WebServiceUrl, $ReportFolder, $AvoidDoubleHop, $ReportServerUserName, $ReportServerPassword) + + Write-Verbose ("$logLead : Preparing RDL {0}" -f $reportFile.FullName) + [xml]$reportXml = Get-Content $reportFile + $description = $reportXml.Report.Description + + if ([String]::IsNullOrEmpty($description)) { + + $description = [String]::Empty + } elseif ($description -match "-") { + + $description = $description.Split("-")[0] + } + + Write-Verbose ("$logLead : Report description set to {0}" -f $description) + + Write-Verbose "$logLead : Calling Publish-SSRSReport" + (Publish-SSRSReport -webServiceUrl $WebServiceUrl ` + -rdlPath $reportFile.FullName ` + -reportFolder $ReportFolder ` + -avoidDoubleHop $AvoidDoubleHop ` + -reportServerUserName $ReportServerUserName ` + -reportServerPassword $ReportServerPassword) | Out-Null + } + + if ( $null -ne $ReportHashLocation ) { + $reportHashes = Import-Csv $ReportHashLocation + } else { + $reportHashes = $null + } + + foreach ($reportFile in $reportFiles) { + Write-Verbose ("$logLead : Beginning hash comparison for $($reportFile.Name)") + $reportHash = Get-FileHash $reportFile + + if (($null -ne $reportHashes) -and ($null -ne ($reportHashes | Where-Object { $_.Hash -eq $reportHash.Hash } | Select-Object -First 1))) { + Write-Host ("$logLead : Hash hasn't changed for report {0}. Not republishing." -f $reportFile.Name) + } else { + Write-Host ("$logLead : Starting Load Job for {0}." -f $reportFile.Name) + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $reportFile, $WebServiceUrl, $ReportFolder, $AvoidDoubleHop, $ReportServerUserName, $ReportServerPassword + + $running = @($jobs | Where-Object {$_.State -in ('Running', 'NotStarted')}) + + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) { + Wait-Job -Job $jobs -Any | Out-Null + $running = @($jobs | Where-Object {$_.State -in ('Running', 'NotStarted')}) + } + } + } + + if ($jobs) { + Wait-Job -Job $jobs | Out-Null + } + # Receive-Job to output the logs + $jobs | ForEach-Object { Receive-Job -Job $_ } + + $functionStopWatch.Stop() + Write-Host ("$logLead : Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString()) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.Tests.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.Tests.ps1 new file mode 100644 index 0000000..454a1b1 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.Tests.ps1 @@ -0,0 +1,80 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +describe -Tag 'Unit' 'Publish-SqlReports' { + + it "should not call publish-ssrsreportsdirectory if test-iswebserver == false." { + mock -ModuleName $moduleForMock publish-ssrsreportsdirectory -mockwith { } + + mock -ModuleName $moduleForMock test-path -verifiable { return $true } + mock -ModuleName $moduleForMock test-iswebserver -mockwith { return $false } + + Publish-SqlReports -reportFolder "aws test 6" + Assert-MockCalled -ModuleName $moduleForMock Publish-SSRSReportsDirectory 0 + } + + it "should not call publish-ssrsreportsdirectory if the reportfoldernode is null or empty" { + + # report server path is null + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return "JunkUrl" + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServer" } + + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return $null + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServerPath" } + + mock -ModuleName $moduleForMock publish-ssrsreportsdirectory -mockwith {} + + mock -ModuleName $moduleForMock test-iswebserver -mockwith { return $true } + mock -ModuleName $moduleForMock Test-Path -Verifiable { return $true } + + Publish-SqlReports -reportFolder "aws test 6" + assert-mockcalled -ModuleName $moduleForMock publish-ssrsreportsdirectory 0 + } + + it "should not call publish-ssrsreportsdirectory if the reportserverendpointnode is null or empty" { + # report server url is null + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return $null + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServer" } + + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return "c:\junkPath" + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServerPath" } + + mock -ModuleName $moduleForMock publish-ssrsreportsdirectory -mockwith {} + + mock -ModuleName $moduleForMock test-iswebserver -mockwith { return $true } + + Publish-SqlReports -reportFolder "aws test 6" + assert-mockcalled -ModuleName $moduleForMock publish-ssrsreportsdirectory 0 + } + + it "should call Publish-SSRSReportsDirectory" { + # Full config + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return "JunkUrl" + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServer" } + + mock -ModuleName $moduleForMock Get-AppSetting -MockWith { + return "c:\junkPath" + } -ParameterFilter { $appSettingKey -and $appSettingKey -eq "ReportServerPath" } + + + mock -ModuleName $moduleForMock Publish-SSRSReportsDirectory -MockWith {} + + mock -ModuleName $moduleForMock Test-IsWebserver -MockWith { return $true } + + mock -ModuleName $moduleForMock Test-Path -Verifiable { return $true } + + Publish-SqlReports -ReportFolder "AWS Test 6" + + Assert-MockCalled -ModuleName $moduleForMock Publish-SSRSReportsDirectory + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.ps1 new file mode 100644 index 0000000..3698ba6 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Publish-SqlReports.ps1 @@ -0,0 +1,74 @@ +function Publish-SqlReports { +<# +.SYNOPSIS + Publishes All Sql Reports in a Folder + +.PARAMETER folder + +Alias: ReportFolder +The path which contains the RDL files +.PARAMETER avoidDoubleHop + +Alias: ApplyDoubleHopFix +Embeds the report username and password in the individual RDL files, necessary when SSRS does not run on the SQL Server +#> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [Alias("ReportFolder")] + [ValidateScript( {if (Test-Path $_ -PathType 'Container') { + $true + } else { + throw ("Unable to locate the folder {0}" -f $_) + }})] + [string]$folder, + + [Parameter(Mandatory = $false)] + [Alias("ApplyDoubleHopFix")] + [switch]$avoidDoubleHop + ) + + $logLead = (Get-LogLeadName); + + [System.Reflection.Assembly]::LoadWithPartialName("System.IO") | Out-Null + [System.Reflection.Assembly]::LoadWithPartialName("System.Data") | Out-Null + + try { + if (Test-IsWebServer) { + + $reportServerEndpoint = Get-AppSetting "ReportServer" + $reportFolder = Get-AppSetting "ReportServerPath" + + if (($null -eq $reportFolder) -or ([String]::IsNullOrEmpty($reportFolder))) { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPath"" appSetting from the machine.config.`n`nAutomatic execution cannot continue, but you may still run the script manually by passing the parameters to Publish-SSRSReportsDirectory" + return; + } + if (($null -eq $reportServerEndpoint) -or ([String]::IsNullOrEmpty($reportServerEndpoint))) { + Write-Warning "$logLead : Could not read the value for the ""ReportServer"" appSetting from the machine.config.`n`nAutomatic execution cannot continue, but you may still run the script manually by passing the parameters to Publish-SSRSReportsDirectory" + return; + } + + Write-Host "`n$logLead : Automatically calling Publish-SSRSReportsDirectory with parameters:" + Write-Host ("$logLead : ReportDirectory : {0}" -f $folder) + Write-Host ("$logLead : WebServiceUrl : {0}" -f $reportServerEndpoint) + Write-Host ("$logLead : ReportFolder : {0}`n" -f $reportFolder) + + Publish-SSRSReportsDirectory $folder $reportServerEndpoint $reportFolder -avoidDoubleHop:$AvoidDoubleHop.IsPresent + } else { + Write-Warning "$logLead : This script can only be automatically executed on a web tier server" + Write-Host "$logLead : You may still run the script manually by passing the parameters to Publish-SSRSReportsDirectory" + } + } finally { + if ($DisposeSessions) { + if ($null -ne $SSRSProxy) { + $SSRSProxy.Dispose() + } + + if ($null -ne $SSRSExecutionProxy) { + $SSRSExecutionProxy.Dispose() + } + } + } +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Remove-Datasources.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Remove-Datasources.ps1 new file mode 100644 index 0000000..4a352ca --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Remove-Datasources.ps1 @@ -0,0 +1,72 @@ +function Remove-Datasources { + <# +.SYNOPSIS + Remove DataSource nodes from SSRS Report XML data + +.PARAMETER ReportXml + XML data to remove DataSource nodes from + +.PARAMETER SkipParameterBasedConnections + Whether to keep paramter based DataSources +#> + + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [XML]$ReportXml, + + [Parameter(Position = 1, Mandatory = $false)] + [bool]$SkipParameterBasedConnections = $false + ) + + $logLead = Get-LogLeadName + + $stream = New-Object System.IO.MemoryStream + + try { + Write-Verbose "$logLead : Getting DataSourceReference Elements" + $dataSourceNodes = $ReportXml.GetElementsByTagName("DataSourceReference") + + if (!($SkipParameterBasedConnections)) { + Write-Verbose "$logLead : Adding Parameter Based Datasources to List to Remove" + $dataSourceNodes += $dataSourceNodes = $reportXml.GetElementsByTagName("ConnectionProperties") + } + + $nodesToRemove = @() + + Write-Verbose "$logLead : Rebuilding nodes with bogus information" + foreach ($dataSourceNode in $dataSourceNodes) { + Write-Verbose ("$logLead : Rebuilding Datasource {0}" -f $dataSourceNode.ParentNode.Name) + $connectionProperties = $ReportXml.CreateNode($dataSourceNode.NodeType, "ConnectionProperties", $null) + + $dataProvider = $ReportXml.CreateNode($dataSourceNode.NodeType, "DataProvider", $null) + $dataProvider.InnerText = "SQL" + + $conStr = $ReportXml.CreateNode($DataSourceNode.NodeType, "ConnectString", $null) + $conStr.InnerText = "Data Source=Server Name Here;Initial Catalog=database name here" + + $dataSourceNode.ParentNode.AppendChild($connectionProperties) | Out-Null + + $connectionProperties.AppendChild($dataProvider) | Out-Null + $connectionProperties.AppendChild($conStr) | Out-Null + + $nodesToRemove += $dataSourceNode + } + + Write-Verbose "$logLead : Removing all nodes" + foreach ($node in $nodesToRemove) { + $node.ParentNode.RemoveChild($node) | Out-Null + } + + Write-Verbose "$logLead : Cleaning Namespaces" + $ReportXml.InnerXml = $ReportXml.InnerXml.Replace("xmlns=`"`"", "") + + Write-Verbose "$logLead : Saving ReportData to Stream" + $ReportXml.Save($stream) + return $stream.ToArray() + } finally { + $stream.Close() + $stream.Dispose() + } +} + diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportDataSource.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportDataSource.ps1 new file mode 100644 index 0000000..38ce05e --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportDataSource.ps1 @@ -0,0 +1,147 @@ +function Set-ReportDataSource { +<# +.SYNOPSIS + Set the DataSource for a SSRS report + +.PARAMETER Proxy + SSRS Proxy + +.PARAMETER ReportPath + Path to report on SSRS server + +.PARAMETER RdlPath + Local filepath to RDL report file + +.PARAMETER AvoidDoubleHop + Whether to use special measures to avoid the DoubleHop issue + +.PARAMETER ReportServerUserName + Username for SSRS server + +.PARAMETER ReportServerPassword + Password for SSRS server +#> + + [CmdletBinding()] + Param( + [Parameter(Position=0,Mandatory=$true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$Proxy, + + [Parameter(Position=1, Mandatory=$true)] + [string]$ReportPath, + + [Parameter(Position=2, Mandatory=$true)] + [string]$RdlPath, + + [Parameter(Position=3, Mandatory=$true)] + [Alias("ApplyDoubleHopFix")] + [bool]$AvoidDoubleHop, + + [Parameter(Position=4, Mandatory=$false)] + [Alias("Username")] + [string]$ReportServerUserName, + + [Parameter(Position=5, Mandatory=$false)] + [Alias("Password")] + [string]$ReportServerPassword + ) + + $logLead = Get-LogLeadName + + Write-Verbose ("$logLead : Getting XML Datasources of {0}" -f $RdlPath) + [xml]$reportContent = Get-Content $RdlPath + $rdlDataSources = $reportContent.Report.DataSources + + Write-Verbose ("$logLead : Getting Datasource References of published report {0} " -f $ReportPath) + $reportDataSources = $Proxy.GetItemDataSources($ReportPath) + + Write-Verbose "$logLead : Calling Get-PublishedDataSources" + $publishedDataSources = Get-PublishedDataSources $Proxy ($ReportPath.Split("/") | Where-Object {$_ -ne [String]::Empty} | Select-Object -First 1) + + $dirty = $false + + foreach ($rdlDataSource in $rdlDataSources.DataSource) { + Write-Verbose ("$logLead : Checking Datasource {0}" -f $rdlDataSource.Name) + $targetDataSource = $reportDataSources | Where-Object {$_.Name -eq $rdlDataSource.Name} + + if ($null -eq $targetDataSource) { + Write-Verbose ("$logLead : Unable to find data source in published report with name {0}" -f $rdlDataSource.Name) + continue + } + + $ProxyNamespace = $targetDataSource.GetType().Namespace + + if ($null -eq $rdlDataSource.DataSourceReference) { + Write-Verbose "$logLead : Setting DataProvider Reference" + + $ref = New-Object $targetDataSource.GetType().Assembly.GetType("$Proxynamespace.DataSourceDefinition") + $ref.ConnectString = $rdlDataSource.ConnectionProperties.ConnectString + $ref.Extension = $rdlDataSource.ConnectionProperties.DataProvider + $ref.OriginalConnectStringExpressionBased = ($rdlDataSource.ConnectionProperties.ConnectString -match "^=").ToString() + $ref.Enabled = $true + $ref.EnabledSpecified = $true + $ref.UseOriginalConnectString = $true + $ref.ImpersonateUserSpecified = $true + + if ($rdlDataSource.ConnectionProperties.IntegratedSecurity -match "true") { + if ($AvoidDoubleHop) { + # Set the credentials on this data source + if ((Test-IsWebServer) -and ([String]::IsNullOrEmpty($ReportServerUserName) -or [String]::IsNullOrEmpty($ReportServerPassword))) { + # Read the User Credentials from the machine.config + Write-Verbose ("$logLead : Reading Credential Information from the machine.config") + [xml]$config = Get-ReportServerConfiguration + $reportServerUserNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerUserName""]/@value") + $ReportServerPasswordNode = $config.appSettings.SelectSingleNode("//add[@key=""ReportServerPassword""]/@value") + + if (($null -eq $reportServerUserNode) -or ([String]::IsNullOrEmpty($reportServerUserNode.Value))) { + Write-Warning "$logLead : Could not read the value for the ""ReportServerUserName"" appSetting from the machine.config. Correct this report manually" + return; + } + + if (($null -eq $ReportServerPasswordNode) -or ([String]::IsNullOrEmpty($ReportServerPasswordNode.Value))) { + Write-Warning "$logLead : Could not read the value for the ""ReportServerPassword"" appSetting from the machine.config. Correct this report manually" + return; + } else { + Write-Verbose ("$logLead : Using Credentials for $ReportServerUserName Passed via Parameter") + } + + $ReportServerUserName = $reportServerUserNode.Value + $ReportServerPassword = $ReportServerPasswordNode.Value + } + + Write-Verbose "$logLead : Setting DataSource to Use Username $ReportServerUserName" + $ref.UserName = $ReportServerUserName + $ref.Password = $ReportServerPassword + $ref.WindowsCredentials = $true + } + + $ref.CredentialRetrieval = [SSRS.CredentialRetrievalEnum]::Store + } + + Write-Host ("$logLead : Setting datasource {0} for report {1} " -f $targetDataSource.Name, $RdlPath) + $targetDataSource.Item = $ref + $dirty = $true + } else { + Write-Verbose "$logLead : Setting DataSource Reference" + + $ref = New-Object $targetDataSource.GetType().Assembly.GetType("$Proxynamespace.DataSourceReference") + $ref.Reference = $publishedDataSources | Where-Object {$_.Name -eq $rdlDataSource.Name} | Select-Object -First 1 -ExpandProperty "Path" + + if ($ref.Reference.Length -gt 0) { + Write-Verbose ("Setting datasource {0} for report {1} " -f $targetDataSource.Name, $RdlPath) + $targetDataSource.Item = $ref + $dirty = $true + } + } + } + + if ($dirty) { + Write-Verbose "$logLead : Saving updated datasources" + + try { + $Proxy.SetItemDataSources($ReportPath, $reportDataSources) + } catch [Exception] { + Write-Warning ("$logLead : An invalid parameter was specified for one or more datasources. Correct this report manually. Exception: {0}" -f $_.Exception.Message) + } + } +} diff --git a/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportParameters.ps1 b/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportParameters.ps1 new file mode 100644 index 0000000..ef1f604 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/Public/Set-ReportParameters.ps1 @@ -0,0 +1,62 @@ +function Set-ReportParameters { + <# +.SYNOPSIS + Set SSRS report parameters from local RDL file on remote SSRS report + +#> + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$Proxy, + + [CmdletBinding()] + [Parameter(Position = 1, Mandatory = $true)] + [System.Web.Services.Protocols.SoapHttpClientProtocol]$SSRSExecutionProxy, + + [Parameter(Position = 2, Mandatory = $true)] + [string]$ReportPath, + + [Parameter(Position = 3, Mandatory = $true)] + [string]$RdlPath + ) + + $logLead = Get-LogLeadName + + Write-Verbose ("$logLead : Getting RDL Content from {0}" -f $RdlPath) + [xml]$reportContent = Get-Content $RdlPath + + $reportParameters = @() + $namespace = $Proxy.GetType().Namespace + + Write-Verbose "$logLead : Calling Remove-DataSources" + $reportData = Remove-Datasources $reportContent + + Write-Verbose "$logLead : Loading Report Execution Definition" + $warnings = $null + + try { + $reportInfo = $SSRSExecutionProxy.LoadReportDefinition($reportData, [ref]$warnings) + } catch { + Write-Warning "$logLead : --- An error occurred reading the report parameters. Let the reports developer know and manually validate the report parameters." + return + } + + Write-Verbose "$logLead : Cloning Report Parameters" + foreach ($parameter in $reportInfo.Parameters) { + $itemParameter = New-Object $Proxy.GetType().Assembly.GetType("$namespace.ItemParameter") + $itemParameter = Copy-ObjectProperties $parameter $itemParameter @("ValidValues") + + foreach ($value in $parameter.ValidValues) { + $validValue = New-Object $Proxy.GetType().Assembly.GetType("$namespace.ValidValue") + Copy-ObjectProperties $value $validValue + + $itemParameter.ValidValues += $validValue + } + + $reportParameters += $itemParameter + } + + Write-Verbose "$logLead : Using Proxy to Set Report Parameters" + $Proxy.SetItemParameters($ReportPath, $reportParameters) +} + diff --git a/Modules/Alkami.DevOps.SqlReports/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.SqlReports/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SqlReports/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.SqlReports/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.DevOps.SqlReports/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.nuspec b/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.nuspec new file mode 100644 index 0000000..da8ebb8 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.nuspec @@ -0,0 +1,33 @@ + + + + Alkami.DevOps.SystemEngineering + $version$ + Alkami Platform Modules - SystemEngineering - Functional + Alkami Technology + Alkami Technology + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.SystemEngineering + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the SRE SysEng module for use with PowerShell. + + PowerShell + Copyright (c) 2020 Alkami Technology + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.psd1 b/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.psd1 new file mode 100644 index 0000000..d7bcdbc --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Alkami.DevOps.SystemEngineering.psd1 @@ -0,0 +1,20 @@ +@{ + RootModule = 'Alkami.DevOps.SystemEngineering.psm1' + ModuleVersion = '1.4.0' + GUID = '5151a169-c763-489e-8213-b437b7a294b1' + Author = 'Alkami' + CompanyName = 'Alkami Technology, Inc.' + Copyright = '(c) 2020 Alkami Technology, Inc.. All rights reserved.' + Description = 'A set of common functions typically used by SystemEngineering' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.DevOps.Common','Alkami.PowerShell.AD' + FunctionsToExport = 'Disable-ActiveDirectoryAccount','Disable-AlkamiDomainAccounts','Export-ACMCertificatesByName','Get-ACMCertificateBindingList','Get-ACMCertificateDetailsListByName','Get-ActiveDirectoryAccount','Get-BitLockerRecoveryKeys','Get-DnsByIP','Get-DomainNameDistinguishedName','Get-SecurityGroupsForUser','Get-TerminatedComputersReport','Get-WorkspaceBundleList','Move-AccountToDisabledOU','New-GMSAStack','New-SecurePassword','New-ServerlessServiceAccountPair','New-SftpPasswordHash','New-SftpUser','Test-IsUserDomainAdmin','Update-AWSProfile','Update-SftpPassword','Write-AlkamiSecretResourcePolicy' + AliasesToExport = '' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'syseng') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.SystemEngineering+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.SystemEngineering+Module' +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/AlkamiManifest.xml b/Modules/Alkami.DevOps.SystemEngineering/AlkamiManifest.xml new file mode 100644 index 0000000..0df0101 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.SystemEngineering + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.ps1 new file mode 100644 index 0000000..578818b --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.ps1 @@ -0,0 +1,42 @@ +function Get-AlkamiIamAssumeRolePolicyString { +<# +.SYNOPSIS + Returns the string for an AWS IAM assume role policy. + +.PARAMETER ServiceName + [string] The AWS service name to grant sts:AssumeRole to in the policy (e.g. 'ec2', 'ecs-task'). + +.EXAMPLE + Get-AlkamiIamAssumeRolePolicyString -ServiceName 'ec2' + +{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Sid":"AllowEcsAssumeRole","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]} +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ServiceName + ) + + $actualServiceName = $ServiceName + if ( $false -eq $actualServiceName.EndsWith('.amazonaws.com') ) { + $actualServiceName += '.amazonaws.com' + } + + $policyObj = @{ + Version = "2012-10-17" + Statement = @( + @{ + Sid = "AllowAwsServiceAssumeRole" + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = @{ + Service = $actualServiceName + } + } + ) + } + + return (ConvertTo-Json -InputObject $policyObj -Compress -Depth 10) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.tests.ps1 new file mode 100644 index 0000000..7f303dc --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiIamAssumeRolePolicyString.tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + + Describe "Get-AlkamiIamAssumeRolePolicyString" { + + Context "Parameter Validation" { + + It "Throws if ServiceName is Null" { + { Get-AlkamiIamAssumeRolePolicyString -ServiceName $null } | Should -Throw + } + + It "Throws if ServiceName is Empty" { + { Get-AlkamiIamAssumeRolePolicyString -ServiceName '' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command Get-AlkamiIamAssumeRolePolicyString).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Returns a String With a Valid JSON Conversion" { + + { ConvertFrom-Json (Get-AlkamiIamAssumeRolePolicyString -ServiceName 'test') } | Should -Not -Throw + } + + It "Appends '.amazonaws.com' If Not Provided" { + + $result = ConvertFrom-Json (Get-AlkamiIamAssumeRolePolicyString -ServiceName 'test') + $result.Statement[0].Principal.Service | Should -BeExactly 'test.amazonaws.com' + } + + It "Does Not Append '.amazonaws.com' If Provided" { + + $result = ConvertFrom-Json (Get-AlkamiIamAssumeRolePolicyString -ServiceName 'test.amazonaws.com') + $result.Statement[0].Principal.Service | Should -BeExactly 'test.amazonaws.com' + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.ps1 new file mode 100644 index 0000000..6c694a7 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.ps1 @@ -0,0 +1,69 @@ +function Get-AlkamiSecretResourcePolicyString { +<# +.SYNOPSIS + Returns the string for an AWS Secret resource policy that allows access to admins and SysEng (by default). + +.PARAMETER ProfileName + [string] The AWS profile to use during when creating the resource policy. + +.PARAMETER SecretAccessExtraArns + [string[]] An array of AWS ARNs allowed to access the secret in addition to the defaults. + +.EXAMPLE + Get-AlkamiSecretResourcePolicyString -ProfileName 'temp-dev' + +{"Version":"2012-10-17","Statement":[{"Action":"secretsmanager:*","Condition":{"ArnNotEquals":{"aws:PrincipalArn":["arn:aws:iam::327695573722:role/CLI-SRE-Admin","arn:aws:iam::327695573722:role/DAG-AWS-Admins","arn:aws:iam::327695573722:root"]}},"Principal":{"AWS":"*"},"Resource":"*","Effect":"Deny","Sid":"DenyAllUnlessExplicitlyAllowed"}]} + +.EXAMPLE + Get-AlkamiSecretResourcePolicyString -ProfileName 'temp-dev' -SecretAccessExtraArns @( 'ExampleArn1', 'ExampleArn2' ) + +{"Version":"2012-10-17","Statement":[{"Action":"secretsmanager:*","Condition":{"ArnNotEquals":{"aws:PrincipalArn":["arn:aws:iam::327695573722:role/CLI-SRE-Admin","arn:aws:iam::327695573722:role/DAG-AWS-Admins","arn:aws:iam::327695573722:root","ExampleArn1","ExampleArn2"]}},"Principal":{"AWS":"*"},"Resource":"*","Effect":"Deny","Sid":"DenyAllUnlessExplicitlyAllowed"}]} +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $false)] + [string[]] $SecretAccessExtraArns = $null + ) + + Import-AWSModule + + $accountNumber = (Get-STSCallerIdentity -ProfileName $ProfileName).Account + + $policyObj = @{ + Version = "2012-10-17" + Statement = @( + @{ + Sid = "DenyAllUnlessExplicitlyAllowed" + Action = "secretsmanager:*" + Effect = "Deny" + Resource = "*" + Principal = @{ + AWS = "*" + } + Condition = @{ + ArnNotEquals = @{ + "aws:PrincipalArn" = @( + "arn:aws:iam::${accountNumber}:role/CLI-SRE-Admin", + "arn:aws:iam::${accountNumber}:role/DAG-AWS-Admins", + "arn:aws:iam::${accountNumber}:root" + ) + } + } + } + ) + } + + # Add any extra ARNs that need access to the secret. + foreach ( $extraArn in $SecretAccessExtraArns ) { + if ( $false -eq [string]::IsNullOrWhitespace($extraArn)) { + $policyObj.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' += $extraArn + } + } + + return (ConvertTo-Json -InputObject $policyObj -Compress -Depth 10) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.tests.ps1 new file mode 100644 index 0000000..ce0d81f --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-AlkamiSecretResourcePolicyString.tests.ps1 @@ -0,0 +1,81 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "Get-AlkamiSecretResourcePolicyString" { + + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Get-STSCallerIdentity -ModuleName $inScopeModule -MockWith { + return @{ Account = '123456' } + } + + Context "Parameter Validation" { + + It "Throws if ProfileName is Null" { + { Get-AlkamiSecretResourcePolicyString -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName is Empty" { + { Get-AlkamiSecretResourcePolicyString -ProfileName '' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command Get-AlkamiSecretResourcePolicyString).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Returns a String With a Valid JSON Conversion" { + + { ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test') } | Should -Not -Throw + } + + It "Includes CLI-SRE-Admin Role By Default" { + + $searchTerm = 'role/CLI-SRE-Admin' + $jsonResult = ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test') + $searchResult = $jsonResult.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' | Where-Object { $_.EndsWith($searchTerm) } + $searchResult | Should -Not -BeNull + } + + It "Includes DAG-AWS-Admins Role By Default" { + + $searchTerm = 'role/DAG-AWS-Admins' + $jsonResult = ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test') + $searchResult = $jsonResult.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' | Where-Object { $_.EndsWith($searchTerm) } + $searchResult | Should -Not -BeNull + } + + It "Includes Account Root User By Default" { + + $searchTerm = 'root' + $jsonResult = ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test') + $searchResult = $jsonResult.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' | Where-Object { $_.EndsWith($searchTerm) } + $searchResult | Should -Not -BeNull + } + + It "Includes No Other AWS Entites By Default" { + + $exclusionTerms = 'CLI-SRE-Admin|CLI-SRE-SysAdministrator|DAG-AWS-Admins|DAG-AWS-SRE-Infrastructure|root' + $jsonResult = ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test') + $searchResult = $jsonResult.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' | Where-Object { $_ -notmatch $exclusionTerms } + $searchResult | Should -BeNull + } + + It "Includes Extra Parameter Values If Provided" { + + $searchTerm = "TestArn" + $jsonResult = ConvertFrom-Json (Get-AlkamiSecretResourcePolicyString -ProfileName 'test' -SecretAccessExtraArns @($searchTerm)) + $jsonResult.Statement.Condition.ArnNotEquals.'aws:PrincipalArn' | Should -Contain $searchTerm + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.ps1 new file mode 100644 index 0000000..96317d0 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.ps1 @@ -0,0 +1,38 @@ +function Get-ServerlessServiceAccountIamPolicyString { +<# +.SYNOPSIS + Returns the string for an AWS IAM policy for serverless service accounts. + +.PARAMETER SecretArns + [string[]] The AWS ARNs for the secrets associated with the serverless service account. + +.EXAMPLE + Get-ServerlessServiceAccountIamPolicyString -SecretArns @( 'example' ) + +{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Sid":"AllowSecretsManagerAccess","Resource":["example"],"Action":["secretsmanager:DescribeSecret","secretsmanager:GetSecretValue"]}]} +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]] $SecretArns + ) + + $policyObj = @{ + Version = "2012-10-17" + Statement = @( + @{ + Sid = "AllowSecretsManagerAccess" + Effect = "Allow" + Action = @( + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ) + Resource = $SecretArns + } + ) + } + + return (ConvertTo-Json -InputObject $policyObj -Compress -Depth 10) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.tests.ps1 new file mode 100644 index 0000000..e156b3a --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-ServerlessServiceAccountIamPolicyString.tests.ps1 @@ -0,0 +1,56 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + + Describe "Get-ServerlessServiceAccountIamPolicyString" { + + Context "Parameter Validation" { + + It "Throws if SecretArns is Null" { + { Get-ServerlessServiceAccountIamPolicyString -SecretArns $null } | Should -Throw + } + + It "Throws if SecretArns is Empty" { + { Get-ServerlessServiceAccountIamPolicyString -SecretArns @() } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command Get-ServerlessServiceAccountIamPolicyString).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Returns a String With a Valid JSON Conversion" { + + { ConvertFrom-Json (Get-ServerlessServiceAccountIamPolicyString -SecretArns @('test')) } | Should -Not -Throw + } + + It "Allows DescribeSecret Action" { + + $result = ConvertFrom-Json (Get-ServerlessServiceAccountIamPolicyString -SecretArns @('test')) + $result.Statement[0].Action | Should -Contain 'secretsmanager:DescribeSecret' + } + + It "Allows GetSecretValue Action" { + + $result = ConvertFrom-Json (Get-ServerlessServiceAccountIamPolicyString -SecretArns @('test')) + $result.Statement[0].Action | Should -Contain 'secretsmanager:GetSecretValue' + } + + It "Grants Access To Specified Resource(s)" { + + $result = ConvertFrom-Json (Get-ServerlessServiceAccountIamPolicyString -SecretArns @('test1', 'test2')) + $result.Statement[0].Resource | Should -HaveCount 2 + $result.Statement[0].Resource | Should -Contain 'test1' + $result.Statement[0].Resource | Should -Contain 'test2' + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.ps1 new file mode 100644 index 0000000..794f8c1 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.ps1 @@ -0,0 +1,124 @@ +function Get-SftpUserDefaultSecretString { + <# +.SYNOPSIS + Returns the default string for an SFTP user AWS Secret. + +.DESCRIPTION + Returns the default string for an SFTP user AWS Secret. This structure must match exactly the expectations of the SFTP Authentication Lambda. + +.PARAMETER BucketName + [string] The target SFTP S3 Bucket name for the environment. + +.PARAMETER HomeDirSuffix + [string] The relative path in the target SFTP S3 bucket to jail the user's home directory. + +.PARAMETER KmsArn + [string] The ARN of the KMS key used for SFTP S3 bucket object encryption for the environment. + +.PARAMETER RoleArn + [string] The ARN of the IAM role used by the SFTP Transfer Server for the environment. + +.PARAMETER PasswordHash + [string] The hashed password for the SFTP user. +#> + + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $BucketName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $HomeDirSuffix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $KmsArn, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $RoleArn, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $PasswordHash + ) + + $policyObj = @{ + Version = '2012-10-17' + Statement = @( + @{ + Sid = 'KMSAccess' + Action = @( + 'kms:Decrypt', + 'kms:Encrypt', + 'kms:GenerateDataKey' + ) + Effect = 'Allow' + Resource = $KmsArn + }, + @{ + Sid = 'AllowListingOfUserFolder' + Action = @( + 's3:ListBucket' + ) + Effect = 'Allow' + Resource = @( + "arn:aws:s3:::$BucketName" + ) + Condition = @{ + StringLike = @{ + 's3:prefix' = @( + "$HomeDirSuffix/*", + "$HomeDirSuffix" + ) + } + } + }, + @{ + Sid = 'AWSTransferRequirements' + Effect = 'Allow' + Action = @( + 's3:ListAllMyBuckets', + 's3:GetBucketLocation' + ) + Resource = '*' + }, + @{ + Sid = 'HomeDirObjectAccess' + Effect = 'Allow' + Action = @( + 's3:PutObject', + 's3:GetObject', + 's3:DeleteObjectVersion', + 's3:DeleteObject', + 's3:GetObjectVersion' + ) + Resource = @( + "arn:aws:s3:::$BucketName/$HomeDirSuffix/*" + ) + } + ) + } + + $homeDirObj = @( + @{ + Entry = '/' + Target = "/$BucketName/$HomeDirSuffix" + } + ) + + $policyStr = (ConvertTo-Json -InputObject $policyObj -Compress -Depth 10) + $homeDirStr = (ConvertTo-Json -InputObject $homeDirObj -Compress -Depth 10) + + $object = @{ + Password = $PasswordHash + Role = $RoleArn + Policy = $policyStr + HomeDirectoryDetails = $homeDirStr + } + + return (ConvertTo-Json -InputObject $object) +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.tests.ps1 new file mode 100644 index 0000000..5a8ac51 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-SftpUserDefaultSecretString.tests.ps1 @@ -0,0 +1,95 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +InModuleScope 'Alkami.DevOps.SystemEngineering' { + Describe 'Get-SftpUserDefaultSecretString' { + + Context 'Parameter Validation' { + + It 'Throws if BucketName is Null' { + { Get-SftpUserDefaultSecretString -BucketName $null } | Should -Throw + } + + It 'Throws if BucketName is Empty' { + { Get-SftpUserDefaultSecretString -BucketName '' } | Should -Throw + } + + It 'Throws if HomeDirSuffix is Null' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix $null } | Should -Throw + } + + It 'Throws if HomeDirSuffix is Empty' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix '' } | Should -Throw + } + + It 'Throws if KmsArn is Null' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn $null } | Should -Throw + } + + It 'Throws if KmsArn is Empty' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn '' } | Should -Throw + } + + It 'Throws if RoleArn is Null' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn $null } | Should -Throw + } + + It 'Throws if RoleArn is Empty' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn '' } | Should -Throw + } + + It 'Throws if PasswordHash is Null' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash $null } | Should -Throw + } + + It 'Throws if PasswordHash is Empty' { + { Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash '' } | Should -Throw + } + } + + Context 'Logic' { + + It 'Uses Provided Value for PasswordHash' { + $result = Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash 'TestPassword' + + $resultObj = ConvertFrom-Json $result + $resultObj.Password | Should -BeExactly 'TestPassword' + } + + It 'Uses Provided Value for RoleArn' { + $result = Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash 'TestPassword' + + $resultObj = ConvertFrom-Json $result + $resultObj.Role | Should -BeExactly 'TestRoleArn' + } + + It 'Uses Provided Value for KmsArn' { + $result = Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash 'TestPassword' + + $resultObj = ConvertFrom-Json $result + $resultObj.Policy | Should -Match 'TestKmsArn' + } + + It 'Uses Provided Value for BucketName' { + $result = Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash 'TestPassword' + + $resultObj = ConvertFrom-Json $result + $resultObj.Policy | Should -Match 'TestPrefix' + $resultObj.HomeDirectoryDetails | Should -Match 'TestPrefix' + } + + It 'Uses Provided Value for HomeDirSuffix' { + $result = Get-SftpUserDefaultSecretString -BucketName 'TestPrefix' -HomeDirSuffix 'TestSuffix' -KmsArn 'TestKmsArn' -RoleArn 'TestRoleArn' -PasswordHash 'TestPassword' + + $resultObj = ConvertFrom-Json $result + $resultObj.Policy | Should -Match 'TestSuffix' + $resultObj.HomeDirectoryDetails | Should -Match 'TestSuffix' + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.ps1 new file mode 100644 index 0000000..d1dc4a1 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.ps1 @@ -0,0 +1,52 @@ +function Get-YeatsLambdaIamRoleArn { +<# +.SYNOPSIS + Retrieves the environment-specific ARN for the Yeats rotation Lambda IAM role. + +.PARAMETER EnvironmentTag + [string] The 'alk:env' value for the Yeats Lambda IAM role name. + +.PARAMETER ProfileName + [string] The AWS profile to use during search operation. + +.EXAMPLE + Get-YeatsLambdaIamRoleArn -EnvironmentTag 'qashared' -ProfileName 'temp-qa' + +arn:aws:iam::668894625708:role/atlantis-generated-alk-qashared-services-yeats-lambdarole +#> + + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $EnvironmentTag, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName + ) + + $logLead = Get-LogLeadName + $result = $null + + Import-AWSModule + + # Build the role name. All of this is constant except for the environment designator. + $roleName = "atlantis-generated-alk-${EnvironmentTag}-services-yeats-lambdarole" + Write-Verbose "$logLead : Calculated Yeats Lambda IAM role name as '$roleName'" + + try { + + $role = Get-IamRole -RoleName $roleName -ProfileName $ProfileName + $result = $role.Arn + + Write-Verbose "$logLead : Yeats Lambda IAM role ARN = '$result'" + + } catch { + + Write-Warning "$logLead : Unable to find Yeats Lambda IAM role named '$roleName' with provided input.`nError encountered was: '$PSItem'" + } + + return $result +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.tests.ps1 new file mode 100644 index 0000000..6b5903c --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsLambdaIamRoleArn.tests.ps1 @@ -0,0 +1,80 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "Get-YeatsLambdaIamRoleArn" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'Get-YeatsLambdaIamRoleArn.tests' } + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Write-Warning -ModuleName $inScopeModule -MockWith {} + + Context "Parameter Validation" { + + It "Throws if EnvironmentTag is Null" { + { Get-YeatsLambdaIamRoleArn -EnvironmentTag $null } | Should -Throw + } + + It "Throws if EnvironmentName is Empty" { + { Get-YeatsLambdaIamRoleArn -EnvironmentTag '' } | Should -Throw + } + + It "Throws if ProfileName is Null" { + { Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName is Empty" { + { Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName '' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command Get-YeatsLambdaIamRoleArn).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Does not Throw on AWS Exception" { + + Mock -CommandName Get-IamRole -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + { Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName 'temp-test' } | Should -Not -Throw + } + + It "Writes Warning on AWS Exception" { + + Mock -CommandName Get-IamRole -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName 'temp-test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Unable to find Yeats Lambda IAM role' } + } + + It "Returns Null on AWS Exception" { + + Mock -CommandName Get-IamRole -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + $result = Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName 'temp-test' + + $result | Should -BeNull + } + + It "Returns ARN from AWS Results" { + + $testString = "ThisIsAnArn" + Mock -CommandName Get-IamRole -ModuleName $inScopeModule -MockWith { return @{'Arn' = $testString} } + + $result = Get-YeatsLambdaIamRoleArn -EnvironmentTag "test" -ProfileName 'temp-test' + + $result | Should -BeExactly $testString + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.ps1 new file mode 100644 index 0000000..85e8426 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.ps1 @@ -0,0 +1,59 @@ +function Get-YeatsRotationLambdaArn { +<# +.SYNOPSIS + Retrieves the environment-specific ARN for the Yeats rotation Lambda. + +.PARAMETER EnvironmentTag + [string] The 'alk:env' value for the Yeats rotation Lambda name. + +.PARAMETER ProfileName + [string] The AWS profile to use during search operation. + +.PARAMETER Region + [string] The AWS region to use during search operation. + +.EXAMPLE + Get-YeatsRotationLambdaArn -EnvironmentTag 'devshared' -ProfileName 'temp-dev' + +arn:aws:lambda:us-east-1:327695573722:function:alk-devshared-yeats-process-rotation-event +#> + + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $EnvironmentTag, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $false)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region = 'us-east-1' + ) + + $logLead = Get-LogLeadName + $result = $null + + Import-AWSModule + + # Build the Lambda name. All of this is constant except for the environment designator. + $lambdaName = "alk-${EnvironmentTag}-yeats-process-rotation-event" + Write-Verbose "$logLead : Calculated Yeats rotation Lambda name as '$lambdaName'" + + try { + + $lambda = Get-LMFunctionConfiguration -FunctionName $lambdaName -ProfileName $ProfileName -Region $Region + $result = $lambda.FunctionArn + + Write-Verbose "$logLead : Yeats rotation Lambda ARN = '$result'" + + } catch { + + Write-Warning "$logLead : Unable to find Yeats rotation Lambda named '$lambdaName' with provided input.`nError encountered was: '$PSItem'" + } + + return $result +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.tests.ps1 new file mode 100644 index 0000000..3e639bf --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/Get-YeatsRotationLambdaArn.tests.ps1 @@ -0,0 +1,85 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "Get-YeatsRotationLambdaArn" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'Get-YeatsRotationLambdaArn.tests' } + Mock -CommandName Get-AWSRegion -ModuleName $inScopeModule -MockWith { return @{region = 'us-east-1'} } + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Write-Warning -ModuleName $inScopeModule -MockWith {} + + Context "Parameter Validation" { + + It "Throws if EnvironmentTag is Null" { + { Get-YeatsRotationLambdaArn -EnvironmentTag $null } | Should -Throw + } + + It "Throws if EnvironmentName is Empty" { + { Get-YeatsRotationLambdaArn -EnvironmentTag '' } | Should -Throw + } + + It "Throws if ProfileName is Null" { + { Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName is Empty" { + { Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName '' } | Should -Throw + } + + It "Throws if Region is Not In Validation List" { + { Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName 'test' -Region 'us-west-2' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command Get-YeatsRotationLambdaArn).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Does not Throw on AWS Exception" { + + Mock -CommandName Get-LMFunctionConfiguration -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + { Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName 'temp-test' } | Should -Not -Throw + } + + It "Writes Warning on AWS Exception" { + + Mock -CommandName Get-LMFunctionConfiguration -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName 'temp-test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Unable to find Yeats rotation Lambda' } + } + + It "Returns Null on AWS Exception" { + + Mock -CommandName Get-LMFunctionConfiguration -ModuleName $inScopeModule -MockWith { throw "This is a test" } + + $result = Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName 'temp-test' + + $result | Should -BeNull + } + + It "Returns ARN from AWS Results" { + + $testString = "ThisIsAnArn" + Mock -CommandName Get-LMFunctionConfiguration -ModuleName $inScopeModule -MockWith { return @{'FunctionArn' = $testString} } + + $result = Get-YeatsRotationLambdaArn -EnvironmentTag "test" -ProfileName 'temp-test' + + $result | Should -BeExactly $testString + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.ps1 new file mode 100644 index 0000000..e9c3d93 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.ps1 @@ -0,0 +1,131 @@ +function New-ServerlessServiceAccountActiveDirectoryUserPair { +<# +.SYNOPSIS + Create a pair of serverless service accounts in Active Directory. + +.DESCRIPTION + Creates a pair of serverless service accounts in Active Directory and adds the newly + created accounts to the SQL access security group for the appropriate environment. + +.PARAMETER Cred + [PSCredential] A credential object for the FH user that has permissions to create new accounts + on the domain. + +.PARAMETER UserDataList + [PSCredential[]] An array with exactly two credential objects containing the usernames and passwords to use + during account creation. + +.PARAMETER UserOuPathCommon + [string] The OU Path on the domain to create the users within. Formatted as a filesystem path + by the calling module to synchronize the AD implementation with the AWS Secrets Manager implementation. + +.PARAMETER Environment + [string] The target environment for the user accounts. + +.PARAMETER TicketNumber + [string] The Jira ticket identifier requesting the new accounts. + +.EXAMPLE + New-ServerlessServiceAccountActiveDirectoryUserPair -Cred (Get-AlkamiCredential) ` + -UserDataList @(( Get-AlkamiCredential -UserName 'ExampleA' -Password 'ExampleA!1Pass'), ( Get-AlkamiCredential -UserName 'ExampleB' -Password 'ExampleB!1Pass')) ` + -UserOuPathCommon '/ServiceAccounts/Dev' ` + -Environment 'Dev' ` + -TicketNumber 'SYSENG-1234' +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [PSCredential] $Cred, + + [Parameter(Mandatory = $true)] + [ValidateCount(2, 2)] + [PSCredential[]] $UserDataList, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $UserOuPathCommon, + + [Parameter(Mandatory = $true)] + [ValidateSet('Dev', 'Qa', 'LoadTest', 'Staging', 'Prod', IgnoreCase = $false)] + [string] $Environment, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-z]+-\d+$")] + [string]$TicketNumber + ) + + $logLead = (Get-LogLeadName) + $domainName = "fh.local" + $domainNameDn = Get-DomainNameDistinguishedName $domainName + $serviceAccountGroupOuPath = "OU=ServiceAccounts,OU=${Environment},OU=SecurityGroups,${domainNameDn}" + + # Convert the user OU Path into DistinguisedName format + $userOuDnParts = $UserOuPathCommon.Split('/', [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object { "OU=$_" } + [array]::Reverse($userOuDnParts) + $userOuDn = ( "{0},{1}" -f ($userOuDnParts -join ','), $domainNameDn) + Write-Verbose "$logLead : Calculated user OU DistinguishedName as '$userOuDn'" + + # Get the required security group for the user or die trying. + $sqlGroupName = "SQL-$Environment-ServerlessApplicationServices" + $sqlGroup = Get-ADGroup -filter { GroupCategory -eq "Security" -and Name -eq $sqlGroupName } ` + -SearchBase $serviceAccountGroupOuPath ` + -Server $domainName ` + -Credential $Cred + if ( $null -eq $sqlGroup ) { + + $errorMsg = "$logLead : Unable to find Active Directory group named '$sqlGroupName'; verify AD configuration." + Write-Error $errorMsg + throw $errorMsg + } + + # Pre-flight that the users do not exist; die early if they do. + foreach ( $userAcct in $UserDataList ) { + + $userName = $userAcct.UserName + $userSearchResults = Get-ADUser -Filter { Name -eq $userName } -Credential $Cred -Server $domainName -SearchBase $userOuDn + if ( $null -ne $userSearchResults ) { + + $errorMsg = "$logLead : Found pre-existing user named '$userName' at '$userOuDn'; aborting." + Write-Error $errorMsg + throw $errorMsg + } + } + + Write-Host "$logLead : Creating accounts in security group $sqlGroupName" + foreach ($userAcct in $UserDataList) { + + $userName = $userAcct.UserName + + try { + + Write-Verbose "$logLead : Creating User '$userName'" + New-ADUser -AccountPassword $userAcct.Password ` + -CannotChangePassword $false ` + -ChangePasswordAtLogon $false ` + -Credential $Cred ` + -Description $TicketNumber ` + -Enabled $true ` + -Name $userName ` + -PasswordNeverExpires $false ` + -PasswordNotRequired $false ` + -Path $userOuDn ` + -SamAccountName $userName ` + -Server $domainName ` + -UserPrincipalName "$userName@$domainName" + + # Add the new account to the SQL group. + Add-ADGroupMember -Identity $sqlGroup -Members $userName + + # Add the new account to the Disable Interactive Logon group. + Add-ADGroupMember -Identity "Disable Interactive Logon" -Members $userName + + Write-Host "$logLead : Created user '$userName'" + + } catch { + + $errorMsg = "$logLead : Creation of user '$userName' failed. Error encountered was: [$PSItem]" + Write-Error $errorMsg + throw $errorMsg + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.tests.ps1 new file mode 100644 index 0000000..72215f0 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountActiveDirectoryUserPair.tests.ps1 @@ -0,0 +1,125 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "New-ServerlessServiceAccountActiveDirectoryUserPair" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'New-ServerlessServiceAccountActiveDirectoryUserPair.tests' } + Mock -CommandName Get-DomainNameDistinguishedName -ModuleName $inScopeModule -MockWith { return 'DC=fh,DC=local' } + Mock -CommandName Write-Error -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Add-ADGroupMember -ModuleName $inScopeModule -MockWith {} + + $testCredential = New-Object 'Management.Automation.PsCredential' 'Test', ( ConvertTo-SecureString -AsPlainText -Force -String 'Test' ) + + $testList = @() + $testList += ( New-Object 'Management.Automation.PsCredential' 'test1', ( ConvertTo-SecureString -AsPlainText -Force -String 'test1' )) + $testList += ( New-Object 'Management.Automation.PsCredential' 'test2', ( ConvertTo-SecureString -AsPlainText -Force -String 'test2' )) + + Context "Parameter Validation" { + + It "Throws if UserDataList has too few elements" { + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList @() } | Should -Throw + } + + It "Throws if UserDataList has too many elements" { + $badTestList = @() + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test1', ( ConvertTo-SecureString -AsPlainText -Force -String 'test1' )) + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test2', ( ConvertTo-SecureString -AsPlainText -Force -String 'test2' )) + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test3', ( ConvertTo-SecureString -AsPlainText -Force -String 'test3' )) + + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $badTestList } | Should -Throw + } + + It "Throws if UserOuPathCommon Is Null" { + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon $null } | Should -Throw + } + + It "Throws if UserOuPathCommon Is Empty" { + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon '' } | Should -Throw + } + + It "Throws if Environment Is Not In Approved List" { + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' -Environment 'Test' } | Should -Throw + } + + It "Throws if TicketNumber Does Not Match Regex" { + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' -Environment 'Dev' -TicketNumber 'Test!' } | Should -Throw + } + } + + Context "Logic" { + + It "Writes Error and Throws if SQL Group Not Found" { + + Mock -CommandName Get-ADGroup -ModuleName $inScopeModule -MockWith { return $null } + Mock -CommandName Get-ADUser -ModuleName $inScopeModule -MockWith { return $null } + Mock -CommandName New-ADUser -ModuleName $inScopeModule -MockWith {} + + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' ` + -Environment 'Dev' -TicketNumber 'Test-123' } | Should -Throw "Unable to find Active Directory group" + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Unable to find Active Directory group named' } + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADGroup -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADUser -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ADUser -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It + } + + It "Writes Error and Throws if User Already Exists" { + + Mock -CommandName Get-ADGroup -ModuleName $inScopeModule -MockWith { return $true } + Mock -CommandName Get-ADUser -ModuleName $inScopeModule -MockWith { return $true } + Mock -CommandName New-ADUser -ModuleName $inScopeModule -MockWith {} + + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' ` + -Environment 'Dev' -TicketNumber 'Test-123' } | Should -Throw "Found pre-existing user" + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Found pre-existing user named' } + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADGroup -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADUser -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ADUser -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It + } + + It "Writes Error and Throws if User Creation Fails" { + + Mock -CommandName Get-ADGroup -ModuleName $inScopeModule -MockWith { return $true } + Mock -CommandName Get-ADUser -ModuleName $inScopeModule -MockWith { return $null } + Mock -CommandName New-ADUser -ModuleName $inScopeModule -MockWith { throw "Test" } + + { New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' ` + -Environment 'Dev' -TicketNumber 'Test-123' } | Should -Throw "Creation of user 'test1' failed" + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Creation of user 'test1' failed" } + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADGroup -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADUser -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ADUser -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It + } + + It "Creates Users and Adds Users to Group" { + + Mock -CommandName Get-ADGroup -ModuleName $inScopeModule -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADUser -ModuleName $inScopeModule -MockWith { return $null } + Mock -CommandName New-ADUser -ModuleName $inScopeModule -MockWith {} + + New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $testCredential -UserDataList $testList -UserOuPathCommon 'Test' -Environment 'Dev' -TicketNumber 'Test-123' + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADGroup -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ADUser -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ADUser -Times 2 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-ADGroupMember -Times 4 -Exactly -Scope It + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.ps1 new file mode 100644 index 0000000..b9c000b --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.ps1 @@ -0,0 +1,52 @@ +function New-ServerlessServiceAccountIamPolicy { +<# +.SYNOPSIS + Creates and configures an AWS IAM inline policy for an IAM role + that grants read access to the specified secrets. + +.PARAMETER RoleArn + [string] The pre-existing IAM role ARN. + +.PARAMETER ProfileName + [string] The AWS profile to use during policy creation. + +.PARAMETER Region + [string] The AWS region to use during policy creation. + +.PARAMETER SecretArns + [string[]] An array of AWS Secrets Manager secret ARNs to grant access to in the IAM policy. + +.EXAMPLE +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $RoleArn, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]] $SecretArns + ) + + Import-AWSModule + + $inlinePolicy = Get-ServerlessServiceAccountIamPolicyString -SecretArns $SecretArns + + # AWS PowerShell expects the role name, not ARN. + $roleName = $RoleArn.Split("/")[-1] + + Write-IAMRolePolicy -RoleName $roleName ` + -PolicyName "account-secret-access-inline-policy" ` + -PolicyDocument $inlinePolicy ` + -ProfileName $ProfileName ` + -Region $Region +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.tests.ps1 new file mode 100644 index 0000000..76d9b73 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountIamPolicy.tests.ps1 @@ -0,0 +1,74 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "New-ServerlessServiceAccountIamPolicy" { + + Mock -CommandName Get-AWSRegion -ModuleName $inScopeModule -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Get-ServerlessServiceAccountIamPolicyString -ModuleName $inScopeModule -MockWith { return 'testInlinePolicy' } + Mock -CommandName Write-IAMRolePolicy -ModuleName $inScopeModule -MockWith {} + + Context "Parameter Validation" { + + It "Throws if RoleArn Is Null" { + { New-ServerlessServiceAccountIamPolicy -RoleArn $null } | Should -Throw + } + + It "Throws if RoleArn Is Empty" { + { New-ServerlessServiceAccountIamPolicy -RoleArn '' } | Should -Throw + } + + It "Throws if ProfileName Is Null" { + { New-ServerlessServiceAccountIamPolicy -RoleArn 'TestRole' -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName Is Empty" { + { New-ServerlessServiceAccountIamPolicy -RoleArn 'TestRole' -ProfileName '' } | Should -Throw + } + + It "Throws if Region Is Not In Allowable List" { + { New-ServerlessServiceAccountIamPolicy -RoleArn 'TestRole' -ProfileName 'TestProfile' -Region 'Test' } | Should -Throw + } + + It "Throws if SecretArns Is Null" { + { New-ServerlessServiceAccountIamPolicy -RoleArn 'TestRole' -ProfileName 'TestProfile' -Region 'us-east-1' ` + -SecretArns $null } | Should -Throw + } + + It "Throws if SecretArns Is Empty" { + { New-ServerlessServiceAccountIamPolicy -RoleArn 'TestRole' -ProfileName 'TestProfile' -Region 'us-east-1' ` + -SecretArns @() } | Should -Throw + } + } + + Context "Logic" { + + It "Proxies Supplied Secret Arns to Handling Function" { + + $testArns = @( 'TestArn1', 'TestArn2') + + New-ServerlessServiceAccountIamPolicy -RoleArn 'arn:aws::iam/thisisanarn/TestName' -ProfileName 'TestProfile' -Region 'us-east-1' ` + -SecretArns $testArns + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-ServerlessServiceAccountIamPolicyString -Times 1 -Exactly -Scope It ` + -ParameterFilter { ($null -eq (Compare-Object $SecretArns $testArns)) } + } + + It "Applies Inline Policy to the Supplied Role" { + + New-ServerlessServiceAccountIamPolicy -RoleArn 'arn:aws::iam/thisisanarn/TestName' -ProfileName 'TestProfile' -Region 'us-east-1' ` + -SecretArns @( 'TestArn' ) + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-IAMRolePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $RoleName -ceq 'TestName' } + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.ps1 new file mode 100644 index 0000000..99771b0 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.ps1 @@ -0,0 +1,118 @@ +function New-ServerlessServiceAccountSecret { +<# +.SYNOPSIS + Creates and configures an AWS Secrets Manager secret for serverless service accounts. + +.DESCRIPTION + Creates and configures an AWS Secrets Manager secret for serverless service accounts. + The first account will be in secret version 'AWSPREVIOUS' and the second account will be in + secret version 'AWSCURRENT'. + + Configures replication on the created secret if the replication region is non-null, non-empty. + + Does not apply a resource policy to the created secret; this will need to be applied after creation. + +.PARAMETER SecretName + [string] The name of the secret to create. + +.PARAMETER UserDataList + [PSCredential[]] An array with exactly two credential objects containing the usernames and passwords to use + during account creation. + +.PARAMETER EnvironmentTag + [string] The 'alk:env' tag value to apply to the created secret. + +.PARAMETER ProfileName + [string] The AWS profile to use during secret creation. + +.PARAMETER Region + [string] The AWS region to use during user creation. + +.PARAMETER ReplicationRegion + [string] The target AWS region for replicated AWS Secrets Manager secrets. + To disable replication, set this value to null or empty. + +.PARAMETER Description + [string] The description of the created secret. + +.EXAMPLE + New-ServerlessServiceAccountSecret -SecretName 'Example' ` + -UserDataList @(( Get-AlkamiCredential -UserName 'ExampleA' -Password 'ExampleA!1Pass'), ( Get-AlkamiCredential -UserName 'ExampleB' -Password 'ExampleB!1Pass')) ` + -EnvironmentTag 'prodshared' ` + -ProfileName 'temp-prod' ` + -Region 'us-east-1' ` + -ReplicationRegion 'us-west-2' ` + -Description 'Example secret' +#> + [OutputType([string[]])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SecretName, + + [Parameter(Mandatory = $true)] + [ValidateCount(2, 2)] + [PSCredential[]] $UserDataList, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $EnvironmentTag, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyString()] + [ValidateScript({([String]::IsNullOrEmpty($_) -or ($_ -in (Get-AWSRegion).region))})] + [string] $ReplicationRegion, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Description + ) + + Import-AWSModule + + $logLead = (Get-LogLeadName) + $result = @() + + # Create the Secrets Manager object in AWS with the first user's data. + Write-Verbose ( "{0} : Creating secret named '{1}' with {2} data" -f $logLead, $SecretName, $UserDataList[0].UserName ) + $secretString = (ConvertTo-Json -InputObject @{ 'username' = $UserDataList[0].UserName ; 'password' = (Get-PasswordFromCredential $UserDataList[0]) } -Compress -Depth 10) + $tag1 = New-Object -TypeName PSObject -Property @{ Key="alk:project" ; Value="orb" } + $tag2 = New-Object -TypeName PSObject -Property @{ Key="alk:service" ; Value="serverless" } + $tag3 = New-Object -TypeName PSObject -Property @{ Key="alk:env" ; Value="$EnvironmentTag" } + $response = New-SECSecret -SecretString $secretString -Name $SecretName -Description $Description -Tag @($tag1,$tag2, $tag3) ` + -ProfileName $ProfileName -Region $Region + + $result += $response.ARN + + # Write the second user's data to the existing secret. + # AWS will manage the AWSPREVIOUS and AWSCURRENT tags with this call, setting + # us up for future secret rotation efforts. + Write-Verbose ( "{0} : Updating secret value for '{1}' with {2} data" -f $logLead, $SecretName, $UserDataList[1].UserName ) + $secretString = (ConvertTo-Json -InputObject @{ 'username' = $UserDataList[1].UserName ; 'password' = (Get-PasswordFromCredential $UserDataList[1]) } -Compress -Depth 10) + Write-SECSecretValue -SecretString $secretString -SecretId $SecretName -ProfileName $ProfileName -Region $Region | Out-Null + + # Enable replication on the secret (if the operator didn't turn off that feature). + if ( $false -eq [string]::IsNullOrWhitespace( $ReplicationRegion )) { + + $replicaConfig = New-Object Amazon.SecretsManager.Model.ReplicaRegionType + $replicaConfig.Region = $ReplicationRegion + + Write-Verbose "$logLead : Replicating newly created secret to $ReplicationRegion." + $response = Add-SECSecretToRegion -SecretId $SecretName -AddReplicaRegion @($replicaConfig) -ProfileName $ProfileName -Region $Region + + # AWS returns the original secret's ARN, not the replicated secret's ARN. Luckily, the only thing different about the ARN is the region. + $result += ( $response.ARN -replace $Region, $ReplicationRegion ) + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.tests.ps1 new file mode 100644 index 0000000..ce399a6 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Private/New-ServerlessServiceAccountSecret.tests.ps1 @@ -0,0 +1,169 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "New-ServerlessServiceAccountSecret" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'New-ServerlessServiceAccountSecret.tests' } + Mock -CommandName Get-AWSRegion -ModuleName $inScopeModule -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName New-SECSecret -ModuleName $inScopeModule -MockWith { return @{ ARN = 'TestSecretArn' } } + Mock -CommandName Write-SECSecretValue -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Add-SECSecretToRegion -ModuleName $inScopeModule -MockWith { return @{ ARN = 'TestSecretArn' } } + + $testList = @() + $testList += ( New-Object 'Management.Automation.PsCredential' 'test1', ( ConvertTo-SecureString -AsPlainText -Force -String 'test1' )) + $testList += ( New-Object 'Management.Automation.PsCredential' 'test2', ( ConvertTo-SecureString -AsPlainText -Force -String 'test2' )) + + Context "Parameter Validation" { + + It "Throws if SecretName Is Null" { + { New-ServerlessServiceAccountSecret -SecretName $Null } | Should -Throw + } + + It "Throws if SecretName Is Empty" { + { New-ServerlessServiceAccountSecret -SecretName '' } | Should -Throw + } + + It "Throws if UserDataList has too few elements" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList @() } | Should -Throw + } + + It "Throws if UserDataList has too many elements" { + $badTestList = @() + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test1', ( ConvertTo-SecureString -AsPlainText -Force -String 'test1' )) + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test2', ( ConvertTo-SecureString -AsPlainText -Force -String 'test2' )) + $badTestList += ( New-Object 'Management.Automation.PsCredential' 'test3', ( ConvertTo-SecureString -AsPlainText -Force -String 'test3' )) + + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $badTestList } | Should -Throw + } + + It "Throws if EnvironmentTag Is Null" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag $null } | Should -Throw + } + + It "Throws if EnvironmentTag Is Empty" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag '' } | Should -Throw + } + + It "Throws if ProfileName Is Null" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName Is Empty" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName '' } | Should -Throw + } + + It "Throws if Region Is Not In Allowable List" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'Test' } | Should -Throw + } + + It "Throws if ReplicationRegion Is Not In Allowable List" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion 'Test' } | Should -Throw + } + + It "Throws if Description Is Null" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion 'us-east-1' -Description $null } | Should -Throw + } + + It "Throws if Description Is Empty" { + { New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion 'us-east-1' -Description '' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns an Array of Strings" { + + (Get-Command New-ServerlessServiceAccountSecret).OutputType.Type.ToString() | Should -BeExactly "System.String[]" + } + + It "Creates Secret Using Supplied Arguments" { + + New-ServerlessServiceAccountSecret -SecretName 'TestName' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'TestDescription' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-SECSecret -Times 1 -Exactly -Scope It ` + -ParameterFilter { (($Name -ceq 'TestName') -and ($Description -ceq 'TestDescription') -and ` + ($ProfileName -ceq 'temp-test') -and ($Region -ceq 'us-east-1')) } + } + + It "Applies Tags to the Created Secret" { + + New-ServerlessServiceAccountSecret -SecretName 'TestName' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'TestDescription' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-SECSecret -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Tag.Count -gt 0 } + } + + It "Creates Secret Using First User's Data" { + + New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'Test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-SECSecret -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretString -match "test1" } + } + + It "Updates Secret Value Using Second User's Data" { + + New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'Test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-SECSecretValue -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretString -match "test2" } + } + + It "Applies Replication Policy If Replication Region Supplied" { + + New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion 'us-east-1' -Description 'Test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-SECSecretToRegion -Times 1 -Exactly -Scope It ` + -ParameterFilter { $AddReplicaRegion[0].Region -ceq 'us-east-1' } + } + + It "Returns Two ARNs If Replication Region Supplied" { + + $result = New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion 'us-east-1' -Description 'Test' + + $result | Should -HaveCount 2 + } + + It "Does Not Apply Replication Policy If Replication Region Is Null" { + + New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion $null -Description 'Test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-SECSecretToRegion -Times 0 -Exactly -Scope It + } + + It "Does Not Apply Replication Policy If Replication Region Is Empty" { + + New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'Test' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Add-SECSecretToRegion -Times 0 -Exactly -Scope It + } + + It "Returns One ARN If Replication Region Not Supplied" { + + $result = New-ServerlessServiceAccountSecret -SecretName 'Test' -UserDataList $testList -EnvironmentTag 'test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -ReplicationRegion '' -Description 'Test' + + $result | Should -HaveCount 1 + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Private/VariableDeclarations.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.ps1 new file mode 100644 index 0000000..1950ea5 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.ps1 @@ -0,0 +1,51 @@ +function Disable-ActiveDirectoryAccount { + + <# + .SYNOPSIS + Disables a user, MSA, or gMSA account + + .DESCRIPTION + Disables a user, MSA, or gMSA account + + .PARAMETER Accounts + [Microsoft.ActiveDirectory.Management.ADAccount] An ADAccount base object + + .EXAMPLE + Disable-ActiveDirectoryAccount "fake.serviceaccount" + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Microsoft.ActiveDirectory.Management.ADAccount]$Account + ) + + $logLead = Get-LogLeadName + + if (!(Test-IsUserDomainAdmin)) { + + Write-Warning "$logLead : You must have domain administrative privileges to run this command" + return $nulls + } + + $accountName = $Account.Name + + if ($true -eq $Account.Enabled) { + + Write-Host "$logLead : Disabling account [$accountName]" + + if ($Account.DistinguishedName -match "Managed Service Accounts") { + + Write-Verbose "$logLead : MSA/gMSA detected" + Set-ADServiceAccount -Identity $Account.DistinguishedName -Enabled:$false + } else { + + Write-Verbose "$logLead : Standard account detected" + Set-ADUser -Identity $Account.DistinguishedName -Enabled:$false + } + + } else { + + Write-Warning "$logLead : Account [$accountName] already disabled." + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.tests.ps1 new file mode 100644 index 0000000..cbfd491 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-ActiveDirectoryAccount.tests.ps1 @@ -0,0 +1,93 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Import-Module ActiveDirectory + +Describe "Disable-ActiveDirectoryAccount" { + + $fakeAccountName = "FakeyMcFakeAccount" + + function Get-CleanTestUser { + + $fakeAccountName = "FakeyMcFakeAccount" + $testUser = New-Object Microsoft.ActiveDirectory.Management.ADUser + $testUser.DistinguishedName = "CN=$fakeAccountName,CN=Managed Service Accounts,DC=foo,DC=bar" + $testUser.Enabled = $false + $testUser.ObjectClass = "msDS-GroupManagedServiceAccount" + $testUser.ObjectGUID = "deadbeef-dead-beef-dead-beef00000075" + $testUser.SamAccountName = "fake.mcfakeuser$" + $testUser.SID = "S-1-2-34-5678901234-5678901234-5678901234-56789" + $testUser.UserPrincipalName = "" + + # This property is 'read-only' + $testUser.Item('Name').Value = $fakeAccountName + return $testUser + } + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Disable-ActiveDirectoryAccount.tests' } + Mock -CommandName Set-ADUser -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Set-ADServiceAccount -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Context "User Permissions" { + + It "Writes a Warning and Exits Early if the User Does Not Have Domain Admin Rights" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + + $testUser = Get-CleanTestUser + Disable-ActiveDirectoryAccount $testUser + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "You must have domain administrative privileges" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADServiceAccount -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADUser -Times 0 -Exactly -Scope It + } + } + + Context "Logic" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + + It "Writes a Warning and Does Not Disable the User if it is Already Disabled" { + + $testUser = Get-CleanTestUser + Disable-ActiveDirectoryAccount $testUser + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "already disabled" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADServiceAccount -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADUser -Times 0 -Exactly -Scope It + } + + It "Disables the Service Account User if they are Enabled" { + + $testUser = Get-CleanTestUser + $testUser.Enabled = $true + Disable-ActiveDirectoryAccount $testUser + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADServiceAccount -Times 1 -Exactly -Scope It ` + -ParameterFilter { ($Identity -match "$fakeAccountName") -and ($Enabled -eq $false) } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADUser -Times 0 -Exactly -Scope It + } + + It "Disables the Standard Account User if they are Enabled" { + + $testUser = Get-CleanTestUser + $testUser.Enabled = $true + $testUser.DistinguishedName = "CN=$fakeAccountName,CN=Users,DC=foo,DC=bar" + Disable-ActiveDirectoryAccount $testUser + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADUser -Times 1 -Exactly -Scope It ` + -ParameterFilter { ($Identity -match "$fakeAccountName") -and ($Enabled -eq $false) } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ADServiceAccount -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.ps1 new file mode 100644 index 0000000..7681299 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.ps1 @@ -0,0 +1,64 @@ +function Disable-AlkamiDomainAccounts { + + <# + .SYNOPSIS + Disables active directory accounts and moves them to the disabled accounts OU + + .DESCRIPTION + Disables active directory and moves them to the disabled accounts OU. Accounts can be standard accounts or service accounts. + + .PARAMETER Accounts + [string[]] An array of user SAMAccountNames to act upon + + .PARAMETER DisabledAccountOU + [string] The OU name for disabled accounts. Defaults to "Disabled Accounts" + + .PARAMETER DomainName + [string] The domain name to act upon. Defaults to "fh.local" + + .EXAMPLE + Disable-AlkamiServiceAccounts @("fakeuser1", "fakeuser2") + + .EXAMPLE + Disable-AlkamiServiceAccounts @("fakeuser1", "fakeuser2") -DisabledAccountOU "Trash Can" -Domain "corp.alkamitech.com" + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$Accounts, + + [Parameter(Mandatory = $false)] + [string]$DisabledAccountOU = "Disabled Accounts", + + [Parameter(Mandatory = $false)] + [string]$DomainName = "fh.local" + ) + + $logLead = Get-LogLeadName + + if (!(Test-IsUserDomainAdmin)) { + + Write-Warning "$logLead : You must have domain administrative privileges to run this command" + return $null + } + + foreach ($account in $Accounts) { + + Write-Host "$logLead : Processing account [$account]" + $curAccount = Get-ActiveDirectoryAccount -Identity $account + + if ($null -eq $curAccount) { + + Write-Warning "$logLead : Account named [$account] not found; skipping." + continue + } + + # Disable the Account + Disable-ActiveDirectoryAccount -Account $curAccount + + # Move the account to the disabled account OU + Move-AccountToDisabledOU -AccountDistinguishedName $curAccount.DistinguishedName -DisabledAccountOU $DisabledAccountOU -DomainName $DomainName + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.tests.ps1 new file mode 100644 index 0000000..028dacc --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Disable-AlkamiDomainAccounts.tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Disable-AlkamiDomainAccounts" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Disable-AlkamiDomainAccounts.tests' } + Mock -CommandName Disable-ActiveDirectoryAccount -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Move-AccountToDisabledOU -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + $fakeAccountName = "FakeyMcFakeAccount" + + Context "User Permissions" { + + It "Writes a Warning and Exits Early if the User Does Not Have Domain Admin Rights" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-ActiveDirectoryAccount -ModuleName $moduleForMock -MockWith { } + + Disable-AlkamiDomainAccounts @($fakeAccountName) + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "You must have domain administrative privileges" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ActiveDirectoryAccount -Times 0 -Exactly -Scope It + } + } + + Context "Parameter Validation and Manipulation" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock Get-ActiveDirectoryAccount -ModuleName $moduleForMock -MockWith { return $null } + + It "Writes a Warning and Exits if the User Is Not Found" { + + Mock Write-Warning -ModuleName $moduleForMock -MockWith { } + + Disable-AlkamiDomainAccounts @($fakeAccountName) + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Account named \[$fakeAccountName\] not found" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Disable-ActiveDirectoryAccount -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Move-AccountToDisabledOU -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.ps1 new file mode 100644 index 0000000..b0d3575 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.ps1 @@ -0,0 +1,137 @@ +function Export-ACMCertificatesByName { + +<# +.SYNOPSIS + Exports ACM certificates by domain name to the local filesystem. + +.DESCRIPTION + Exports ACM certificates by domain name to the local filesystem. Unfortunately, AWS did not accomodate + this use case when they wrote 'Export-ACMCertificate'. + + Note that this function may generate more than one set of files because domain name uniqueness is not enforced in ACM. + +.PARAMETER DomainName + [string] The domain name of the ACM certificates to retrieve. + +.PARAMETER ProfileName + [string] The AWS profile to use during ACM queries. + +.PARAMETER Region + [string] The AWS region to use during ACM queries. + +.PARAMETER Passphrase + [string] The passphrase to associate with the encrypted exported private key. If not provided, a generated passphrase will be used. + +.PARAMETER GeneratePfx + [switch] Flag indicating whether or not the function should generate a PFX file. Note that this functionality requires OpenSSL + to be installed on your machine; if not installed, this flag will result in an error. + +.EXAMPLE + Export-ACMCertificatesByName -DomainName '*.sandbox.alkami.net' -Passphrase "Secret" -ProfileName 'temp-prod' -Region 'us-east-1' + +.EXAMPLE + Export-ACMCertificatesByName -DomainName '*.sandbox.alkami.net' -ProfileName 'temp-prod' -Region 'us-east-1' -GeneratePfx + +.LINK + https://confluence.alkami.com/x/5ILiBg +#> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $DomainName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $Passphrase, + + [Parameter(Mandatory = $false)] + [switch] $GeneratePfx + ) + + $logLead = (Get-LogLeadName) + + if ( $GeneratePfx ) { + + # Either find OpenSSL or die trying. + $openSslPath = ( Get-Command -Name 'openssl' -CommandType Application -ErrorAction SilentlyContinue -TotalCount 1 ).Source + if ( $null -eq $openSslPath ) { + + Write-Error "$logLead : GeneratePfx flag was provided but OpenSSL command was not found on your system. Please install OpenSSL and retry." + return + } + } + + # Create the top-level temporary directory. + $tempDir = ( Join-Path ([System.IO.Path]::GetTempPath()) 'CertificateExport' ) + New-Item -Path $tempDir -ItemType Directory -Force | Out-Null + + # Sanitize the domain name (if necessary). + $pfxDomainName = $DomainName -replace '\*', '_' + + $certificateDetailsList = Get-ACMCertificateDetailsListByName -DomainName $DomainName -ProfileName $ProfileName -Region $Region + foreach ( $cert in $certificateDetailsList ) { + + Write-Host "$logLead : Processing certificate ARN [$($cert.CertificateArn)]." + + if ( $false -eq $PSBoundParameters.ContainsKey( 'Passphrase' ) ) { + + $actualPassphrase = New-SecurePassword -PasswordLength 15 -ProfileName $ProfileName -Region $Region + Write-Host "$logLead : Generated passphrase for certificate: $actualPassphrase" + + } else { + + $actualPassphrase = $Passphrase + } + + try { + + # Ref: https://docs.aws.amazon.com/powershell/latest/reference/items/Export-ACMCertificate.html + $exportedCert = ( Export-ACMCertificate -CertificateArn $cert.CertificateArn -Passphrase $actualPassphrase.ToCharArray() -ProfileName $ProfileName -Region $Region ) + if ( $null -ne $exportedCert ) { + + $certDirName = ( $cert.CertificateArn -split '/' )[-1] + $certDir = ( Join-Path $tempDir $certDirName ) + $certPath = ( Join-Path $certDir "certificate.pem") + $chainPath = ( Join-Path $certDir "certificate_chain.pem") + $keyPath = ( Join-Path $certDir "private.key") + + # Create the temporary directory for this certificate + New-Item -Path $certDir -ItemType Directory -Force | Out-Null + + Set-Content -Path $certPath -Value $exportedCert.Certificate -Encoding ASCII + Write-Verbose "$logLead : Generated [$certPath]." + + Set-Content -Path $chainPath -Value $exportedCert.CertificateChain -Encoding ASCII + Write-Verbose "$logLead : Generated [$chainPath]." + + Set-Content -Path $keyPath -Value $exportedCert.PrivateKey -Encoding ASCII + Write-Verbose "$logLead : Generated [$keyPath]." + + if ( $GeneratePfx ) { + + $pfxPath = ( Join-Path $certDir "$pfxDomainName.pfx") + + Start-Process -FilePath $openSslPath -Wait -argumentlist "pkcs12 -export -out $pfxPath -in $certPath -inkey $keyPath -certfile $chainPath -passin `"pass:$actualPassphrase`" -passout `"pass:$actualPassphrase`"" + Write-Verbose "$logLead : Generated [$pfxPath]." + } + + Write-Host "$logLead : Files generated at [$certDir]." + } + + } catch { + + Write-Warning "$logLead : Unable to export ACM certificate ARN [$($cert.CertificateArn)] : $($_.Exception.Message)" + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.tests.ps1 new file mode 100644 index 0000000..ee40bcf --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Export-ACMCertificatesByName.tests.ps1 @@ -0,0 +1,130 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Export-ACMCertificatesByName" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Export-ACMCertificatesByName.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Join-Path -ModuleName $moduleForMock -MockWith { return "C:\Test" } + Mock -CommandName New-Item -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Set-Content -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith {return @(@{CertificateArn = 'TestArn'; Serial = 'TestSerial'})} + Mock -CommandName New-SecurePassword -ModuleName $moduleForMock -MockWith { return "GeneratedPW" } + Mock -CommandName Start-Process -ModuleName $moduleForMock -MockWith {} + + Context "Logic" { + + It "Writes Warning If AWS ACM Certificate Export Throws" { + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { throw "Test1" } + + Export-ACMCertificatesByName -DomainName "Test" -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Unable to export ACM certificate ARN \[TestArn\] : Test1" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Export-ACMCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-Content -Times 0 -Exactly -Scope It + } + + It "Generates Three Files On Success Without PFX Flag" { + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { return @{ Certificate = "TestCert"; CertificateChain = "TestChain"; PrivateKey = "TestKey"} } + + Export-ACMCertificatesByName -DomainName "Test" -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Export-ACMCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Set-Content -Times 3 -Exactly -Scope It + } + + It "Writes Error and Aborts If PFX Flag is Present But OpenSSL Is Not Detected" { + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { throw "Test1" } + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $null } + + Export-ACMCertificatesByName -DomainName "Test" -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' -GeneratePfx | Out-Null + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "GeneratePfx flag was provided but OpenSSL command was not found on your system" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-Command -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Export-ACMCertificate -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Set-Content -Times 0 -Exactly -Scope It + } + + It "Generates PFX File On Success If PFX Flag is Present and OpenSSL Is Detected" { + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { return @{ Certificate = "TestCert"; CertificateChain = "TestChain"; PrivateKey = "TestKey" } } + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return @{ Source = "C:\OpenSSL.exe" } } + + Export-ACMCertificatesByName -DomainName "Test" -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' -GeneratePfx | Out-Null + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-Command -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Export-ACMCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Set-Content -Times 3 -Exactly -Scope It + Assert-MockCalled -CommandName Start-Process ` + -ParameterFilter { $FilePath -eq "C:\OpenSSL.exe" } -Times 1 -Exactly -Scope It + } + + It "Sanitizes Domain Name for PFX File" { + + $testDomain = "*.test.com" + $sanitizedDomain = "_.test.com" + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { return @{ Certificate = "TestCert"; CertificateChain = "TestChain"; PrivateKey = "TestKey" } } + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return @{ Source = "C:\OpenSSL.exe" } } + + Export-ACMCertificatesByName -DomainName $testDomain -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' -GeneratePfx | Out-Null + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-Command -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Export-ACMCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Set-Content -Times 3 -Exactly -Scope It + Assert-MockCalled -CommandName Start-Process -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Join-Path ` + -ParameterFilter { $ChildPath -eq "$sanitizedDomain.pfx" } -Times 1 -Exactly -Scope It + } + } + + Context "Inputs" { + + Mock -CommandName Export-ACMCertificate -ModuleName $moduleForMock -MockWith { return @{ Certificate = "TestCert"; CertificateChain = "TestChain"; PrivateKey = "TestKey"} } + + It "Uses Passphrase if Provided" { + + Export-ACMCertificatesByName -DomainName "Test" -Passphrase "TestPW" -ProfileName 'test' -Region 'us-east-1' + + Assert-MockCalled -CommandName Export-ACMCertificate ` + -ParameterFilter { [System.Text.Encoding]::ASCII.GetString($Passphrase) -match 'TestPW' } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-SecurePassword -Times 0 -Exactly -Scope It + } + + It "Uses Generated Passphrase if Not Provided" { + + Export-ACMCertificatesByName -DomainName "Test" -ProfileName 'test' -Region 'us-east-1' + + Assert-MockCalled -CommandName Export-ACMCertificate ` + -ParameterFilter { [System.Text.Encoding]::ASCII.GetString($Passphrase) -match 'GeneratedPW' } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-SecurePassword -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.ps1 new file mode 100644 index 0000000..64506ed --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.ps1 @@ -0,0 +1,161 @@ +function Get-ACMCertificateBindingList { + + <# +.SYNOPSIS + Retrieves the Alkami AWS ACM certificate bindings for all AWS accounts. + +.DESCRIPTION + Retrieves ACM certificate bindings for all AWS accounts and includes the ELB listeners. The script will search all AWS accounts + and each region Alkami has resources in. Alternatively, the example below demonstrates how to limit the search by profile and region. + +.PARAMETER DomainName + The Alkami domain name of the AWS ACM certificates to retrieve. + +.PARAMETER ProfileName + The Alkami AWS profile name to query for ACM certificates. + +.PARAMETER Region + The supported Alkami AWS region in which to query for ACM certificates. + +.EXAMPLE + Get-ACMCertificateBindingList -DomainName '*.dev.alkamitech.com'' -ProfileName 'temp-dev' -Region 'us-east-1' +#> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $DomainName, + + [Parameter(Mandatory = $false) ] + [ValidateScript( { $_ -in (Get-AlkamiAwsProfileList) })] + [string] $ProfileName = $null, + + [Parameter(Mandatory = $false)] + [ValidateScript( { $_ -in (Get-SupportedAwsRegions) })] + [string] $Region = $null + ) + + $output = @() + + $logLead = Get-LogLeadName + + Import-AWSModule + + if ( $PSBoundParameters.ContainsKey('ProfileName')) { + + $profileList = @($ProfileName) + + } else { + + Write-Host "$logLead : User did not provide a ProfileName, using all standard Alkami AWS ProfileNames" + $profileList = Get-AlkamiAwsProfileList + } + + if ($PSBoundParameters.ContainsKey('Region')) { + + $regionList = @($Region) + + } else { + + Write-Host "$logLead : User did not provide a Region, using all supported Alkami AWS Regions" + $regionList = Get-SupportedAwsRegions + } + + foreach ( $curProfile in $profileList ) { + + Write-Host "$logLead : Processing $curProfile" + + foreach ( $curRegion in $regionList ) { + + Write-Host "$logLead : Processing $curRegion" + + try { + + $certList = Get-ACMCertificateDetailsListByName -DomainName $DomainName -ProfileName $curProfile -Region $curRegion + + } catch { + + Write-Warning "$logLead : Unable to retrieve ACM certificate details by name : $($_.Exception.Message)" + Continue + } + + if (Test-IsCollectionNullOrEmpty $certList) { + + Write-Warning "$logLead : No certificates found with a domain name of [$($DomainName)]" + Continue + } + + $apiGatewayDomains = Get-AG2DomainNameList -ProfileName $curProfile -Region $curRegion + + foreach ( $curCert in $certList ) { + + Write-Host "$logLead : Processing $($curCert.CertificateArn) : expires on $($curCert.NotAfter.Date)" + + $tempObject = [pscustomobject]@{ + 'DomainName' = $curCert.DomainName + 'Profile' = $curProfile + 'Region' = $curRegion + 'ARN' = $curCert.CertificateArn + 'NotAfter' = $curCert.NotAfter.Date + 'RenewalEligibility' = $curCert.RenewalEligibility.Value + 'InUseBy' = @() + } + + foreach ( $curUser in $curCert.InUseBy ) { + + Write-Host "$logLead : Cert is in use by ARN: $curUser" + + if ( $curUser -match 'loadbalancer' ) { + + try { + + $elbListeners = Get-ELB2Listener -LoadBalancerArn $curUser -ProfileName $curProfile -Region $curRegion + + } catch { + + Write-Warning "$logLead : Encountered an error retrieving ELB Listener details for $curUser : $($_.Exception.Message)" + $tempObject.InUseBy += $curUser + } + + foreach ( $elbListener in $elbListeners ) { + + try { + + $elbListenerCertList = (Get-ELB2ListenerCertificate -ListenerArn $elbListener.ListenerArn -ProfileName $curProfile -Region $curRegion).CertificateArn + + } catch { + + Write-Warning "$logLead : Error encountered while retrieving ELB Listener certificate list for $($elbListener.ListenerArn): $($_.Exception.Message)" + } + + if ( $elbListenerCertList -contains $curCert.CertificateArn ) { + + Write-Verbose "$logLead : Cert is in use by ELB Listener $($elbListener.ListenerArn)" + $tempObject.InUseBy += $elbListener.ListenerArn + } + } + + } else { + + $tempObject.InUseBy += $curUser + } + } + + $filteredAGDomains = $apiGatewayDomains | Where-Object { $_.DomainNameConfigurations.CertificateArn -eq $curCert.CertificateArn } + foreach ( $agDomain in $filteredAGDomains ) { + + $agMaps = Get-AG2ApiMappingList -DomainName $agDomain.Name -ProfileName $curProfile -Region $curRegion + foreach ( $agMap in $agMaps ) { + $tempObject.InUseBy += "arn:aws:apigateway:$curRegion::/restapis/$($agMap.ApiId)/stages/$($agMap.Stage)" + } + } + + $output += $tempObject + } + } + } + + return $output +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.tests.ps1 new file mode 100644 index 0000000..69b3853 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateBindingList.tests.ps1 @@ -0,0 +1,383 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "InModuleScope - Overriding SUT: $global:functionPath" +Import-Module $global:functionPath -Force +$moduleForMock = '' + +Describe 'Get-ACMCertificateBindingList' { + + Mock -CommandName Get-AlkamiAwsProfileList -ModuleName $moduleForMock -MockWith { return @( 'temp-test1', 'temp-test2' ) } + Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @( 'us-fake-1', 'us-fake-2' ) } + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { return @( @{ 'Region' = 'us-fake-1' }, @{ 'Region' = 'us-fake-2' } ) } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-ACMCertificateBindingList.tests' } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + + Mock -CommandName Get-ELB2ListenerCertificate -ModuleName $moduleForMock -MockWith { + $testObject = @{ + CertificateArn = 'TestCertificateArn' + } + + return @($testObject) + } + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + $testObject = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn' + InUseBy = @( + 'TestCertificateUser', + 'TestCertificateUser-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + return @($testObject) + } + + Mock -CommandName Get-AG2DomainNameList -ModuleName $moduleForMock -MockWith { + $testObject = @{ + Name = 'TestApi' + DomainNameConfigurations = @{ + CertificateArn = 'TestCertificateArn' + } + } + + return @($testObject) + } + + Mock -CommandName Get-ELB2Listener -ModuleName $moduleForMock -MockWith { + + $testObject = @{ + ListenerArn = 'TestListenerArn' + } + + return @($testObject) + } + + Mock -CommandName Get-AG2ApiMappingList -ModuleName $moduleForMock -MockWith { + $testObject = @{ + ApiId = 'TestApiId' + Stage = 'TestStage' + } + + return @($testObject) + } + + Context 'Parameter Validation' { + + It 'Throws if DomainName is Null' { + { Get-ACMCertificateBindingList -DomainName $null } | Should -Throw + } + + It 'Throws if DomainName is Empty' { + { Get-ACMCertificateBindingList -DomainName '' } | Should -Throw + } + + It 'Throws if Profile is Not In Approved List' { + { Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-localtest' } | Should -Throw + } + + It 'Throws if Region is Not In Approved List' { + { Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-test-2' } | Should -Throw + } + } + + Context 'Logic Validation' { + + It 'Uses ProfileName Parameter if Provided' { + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' | Out-Null + + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ProfileName -ceq 'temp-test1' } + } + + It 'Uses All Supported Profiles if ProfileName Parameter is Not Provided' { + + $validProfiles = Get-AlkamiAwsProfileList + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times $validProfiles.Length -Exactly -Scope It ` + -ParameterFilter { $ProfileName -in $validProfiles } + } + + It 'Uses Region Parameter if Provided' { + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' | Out-Null + + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Region -ceq 'us-fake-1' } + } + + It 'Uses All Supported Regions if Region Parameter is Not Provided' { + + $validRegions = Get-SupportedAwsRegions + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times $validRegions.Length -Exactly -Scope It ` + -ParameterFilter { $Region -in $validRegions } + } + + It 'Aborts Processing in Current Region if Get-ACMCertificateDetailsListByName Throws' { + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { throw 'This is an exception.' } + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-ACMCertificateDetailsListByName -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Unable to retrieve ACM certificate details by name' } + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + $testObject = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn' + InUseBy = @( + 'TestCertificateUser', + 'TestCertificateUser-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + return @($testObject) + } + } + + It 'Aborts Processing in Current Region if Get-ACMCertificateDetailsListByName Throws' { + + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AG2DomainNameList -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'No certificates found with a domain name of' } + + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + } + + It 'Prints Warning if Get-ELB2Listener Throws' { + + Mock -CommandName Get-ELB2Listener -ModuleName $moduleForMock -MockWith { throw 'This is an exception.' } + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-ELB2Listener -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ELB2ListenerCertificate -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Encountered an error retrieving ELB Listener details for' } + + Mock -CommandName Get-ELB2Listener -ModuleName $moduleForMock -MockWith { + + $testObject = @{ + ListenerArn = 'TestListenerArn' + } + + return @($testObject) + } + } + + It 'Prints Warning if Get-ELB2ListenerCertificate Throws' { + + Mock -CommandName Get-ELB2ListenerCertificate -ModuleName $moduleForMock -MockWith { throw 'This is an exception.' } + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-ELB2ListenerCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Error encountered while retrieving ELB Listener certificate list' } + + Mock -CommandName Get-ELB2ListenerCertificate -ModuleName $moduleForMock -MockWith { + $testObject = @{ + CertificateArn = 'TestCertificateArn' + } + + return @($testObject) + } + } + + It 'Skips API Gateway Domain Stage Mapping if Certificate Does Not Match' { + + Mock -CommandName Get-AG2DomainNameList -ModuleName $moduleForMock -MockWith { + $testObject = @{ + Name = 'TestApi' + DomainNameConfigurations = @{ + CertificateArn = 'TestCertificateArnNotMatch' + } + } + + return @($testObject) + } + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-AG2DomainNameList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AG2ApiMappingList -Times 0 -Exactly -Scope It + + Mock -CommandName Get-AG2DomainNameList -ModuleName $moduleForMock -MockWith { + $testObject = @{ + Name = 'TestApi' + DomainNameConfigurations = @{ + CertificateArn = 'TestCertificateArn' + } + } + + return @($testObject) + } + } + + It 'Processes all InUseBy Entries for a Certificate' { + + Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose | Out-Null + + Assert-MockCalled -CommandName Get-AG2DomainNameList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ELB2Listener -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ELB2ListenerCertificate -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AG2ApiMappingList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It + } + } + + Context 'Output Validation' { + + It 'Returns a Single Entry When A Single Certificate Is Found' { + + $result = Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose + $result | Should -HaveCount 1 + } + + It 'Returns an Array of InUseBy For Each Certificate User' { + + $result = Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose + $result[0].InUseBy | Should -HaveCount 3 + } + + It 'Returned InUseBy Array Contains ELB ARN if Listener Query Throws' { + + Mock -CommandName Get-ELB2Listener -ModuleName $moduleForMock -MockWith { throw 'This is a test.' } + + $result = Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose + + Assert-MockCalled -CommandName Get-ELB2ListenerCertificate -Times 0 -Exactly -Scope It + + $result | Should -Not -BeNullOrEmpty + $result[0].InUseBy | Should -HaveCount 3 + $result[0].InUseBy | Should -Contain 'TestCertificateUser-loadbalancer' + + Mock -CommandName Get-ELB2Listener -ModuleName $moduleForMock -MockWith { + + $testObject = @{ + ListenerArn = 'TestListenerArn' + } + + return @($testObject) + } + } + + It 'Returns Multiple Entries When Multiple Certificates Are Found' { + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + $testObject1 = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn' + InUseBy = @( + 'TestCertificateUser', + 'TestCertificateUser-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + $testObject2 = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn2' + InUseBy = @( + 'TestCertificateUser2', + 'TestCertificateUser2-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + return @($testObject, $testObject2) + } + + $result = Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose + $result | Should -HaveCount 2 + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + $testObject = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn' + InUseBy = @( + 'TestCertificateUser', + 'TestCertificateUser-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + return @($testObject) + } + } + + It 'Returns An Empty Array When No Certificates Are Found' { + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + return @() + } + + $result = Get-ACMCertificateBindingList -DomainName 'TestDomainName' -ProfileName 'temp-test1' -Region 'us-fake-1' -Verbose + $result | Should -HaveCount 0 + + Mock -CommandName Get-ACMCertificateDetailsListByName -ModuleName $moduleForMock -MockWith { + $testObject = @{ + DomainName = 'TestDomainName' + CertificateArn = 'TestCertificateArn' + InUseBy = @( + 'TestCertificateUser', + 'TestCertificateUser-loadbalancer' + ) + NotAfter = @{ + Date = 'TestDate' + } + RenewalEligibility = @{ + Value = 'TestRenewalEligibility' + } + } + + return @($testObject) + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.ps1 new file mode 100644 index 0000000..7cbcc77 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.ps1 @@ -0,0 +1,81 @@ +function Get-ACMCertificateDetailsListByName { + +<# +.SYNOPSIS + Retrieves a list of ACM certificate details by domain name. + +.DESCRIPTION + Retrieves a list of ACM certificate details by domain name. Unfortunately, AWS did not accomodate + this use case when they wrote 'Get-ACMCertificateDetail' or 'Get-ACMCertificateList', and + 'Get-ACMCertificateList' returns minimal information about the certificates -- just enough to know the + cert exists, but not enough to know anything useful about the certificate. + + Note that this function returns an array because domain name uniqueness is not enforced in ACM. + +.PARAMETER DomainName + [string] The domain name of the ACM certificates to retrieve. + +.PARAMETER ProfileName + [string] The AWS profile to use during ACM queries. + +.PARAMETER Region + [string] The AWS region to use during ACM queries. + +.EXAMPLE + Get-ACMCertificateDetailsListByName -DomainName '*.sandbox.alkami.net' -ProfileName 'temp-prod' -Region 'us-east-1' +#> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $DomainName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule + + try { + + # Ref: https://docs.aws.amazon.com/powershell/latest/reference/items/Get-ACMCertificateList.html + $certList = ( Get-ACMCertificateList -ProfileName $ProfileName -Region $Region ) + + } catch { + + Write-Error "$logLead : Unable to retrieve ACM certificate list from AWS : $($_.Exception.Message)" + return $null + } + + $result = @() + $filteredCertList = $certList | Where-Object { $_.DomainName -eq $DomainName } + foreach ( $cert in $filteredCertList ) { + + try { + + # Ref: https://docs.aws.amazon.com/powershell/latest/reference/items/Get-ACMCertificateDetail.html + $result += ( Get-ACMCertificateDetail -CertificateArn $cert.CertificateArn -ProfileName $ProfileName -Region $Region ) + + } catch { + + Write-Warning "$logLead : Unable to retrieve ACM certificate details for ARN [$($cert.CertificateArn)] : $($_.Exception.Message)" + } + } + + if ( Test-IsCollectionNullOrEmpty -Collection $result ) { + + Write-Warning "$logLead : No certificates found with a domain name of [$DomainName]." + } + + + return [PSObject[]]$result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.tests.ps1 new file mode 100644 index 0000000..f5924e1 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ACMCertificateDetailsListByName.tests.ps1 @@ -0,0 +1,88 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ACMCertificateDetailsListByName" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-ACMCertificateDetailsListByName.tests' } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + + Context "Error Handling" { + + It "Outputs an object of type PSObject[]" { + + (Get-Command Get-ACMCertificateDetailsListByName).OutputType.Type.ToString() | Should -BeExactly "System.Management.Automation.PSObject[]" + + } + + It "Writes Error and Returns Null If ACM Certificate List Throws" { + + Mock -CommandName Get-ACMCertificateList -ModuleName $moduleForMock -MockWith { throw "Test1" } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ACMCertificateDetail -ModuleName $moduleForMock -MockWith { throw "Test2" } + + Get-ACMCertificateDetailsListByName -DomainName "Test" -ProfileName 'test' -Region 'us-east-1' | Should -BeNull + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to retrieve ACM certificate list from AWS .* Test1" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 0 -Exactly -Scope It + } + + It "Writes Warning and Returns Empty Array If ACM Certificate List Returns Null" { + + Mock -CommandName Get-ACMCertificateList -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ACMCertificateDetail -ModuleName $moduleForMock -MockWith { throw "Test2" } + + Get-ACMCertificateDetailsListByName -DomainName "Test" -ProfileName 'test' -Region 'us-east-1' | Should -BeExactly @() + + Assert-MockCalled -CommandName Write-Warning ` + -ParameterFilter { $Message -match "No certificates found" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetail -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + } + + It "Writes Warning and Returns Empty Array If ACM Certificate List Returns Empty Array" { + + Mock -CommandName Get-ACMCertificateList -ModuleName $moduleForMock -MockWith { return @() } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ACMCertificateDetail -ModuleName $moduleForMock -MockWith { throw "Test2" } + + Get-ACMCertificateDetailsListByName -DomainName "Test" -ProfileName 'test' -Region 'us-east-1' | Should -BeExactly @() + + Assert-MockCalled -CommandName Write-Warning ` + -ParameterFilter { $Message -match "No certificates found" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetail -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + } + + It "Writes Warning and Returns Empty Array If ACM Certificate Detail Throws" { + + Mock -CommandName Get-ACMCertificateList -ModuleName $moduleForMock -MockWith { return @(@{ DomainName = 'Test'; CertificateArn = 'TestArn'}) } + Mock -CommandName Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-ACMCertificateDetail -ModuleName $moduleForMock -MockWith { throw "Test2" } + + Get-ACMCertificateDetailsListByName -DomainName "Test" -ProfileName 'test' -Region 'us-east-1' | Should -BeExactly @() + + Assert-MockCalled -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Unable to retrieve ACM certificate details for ARN \[TestArn\] : Test2" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateList -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ACMCertificateDetail -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Test-IsCollectionNullOrEmpty -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.ps1 new file mode 100644 index 0000000..25d7742 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.ps1 @@ -0,0 +1,72 @@ +function Get-ActiveDirectoryAccount { + + <# + .SYNOPSIS + Returns the Active Directory account for a user or service account. + + .DESCRIPTION + Returns the Active Directory account for a user or service account. Caller must have domain admin rights + + .PARAMETER Identity + [string] The identity of the Active Directory account to retrieve. + + .EXAMPLE + Get-ActiveDirectoryAccount -Identity "testUser" + #> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param( + [Parameter(Mandatory)] + [Alias("Account", "AccountName")] + [ValidateNotNullOrEmpty()] + [string]$Identity + ) + + $logLead = (Get-LogLeadName) + $trimIdentity = $Identity.Trim() + + # Make sure the caller passed in more than just whitespace + if ([String]::IsNullOrEmpty($trimIdentity)) { + Write-Warning "$logLead : Identity [$Identity] must contain at least one non-whitespace character." + return $null + } + + # Look for a normal user + try { + + Write-Verbose "$logLead : Attempting to find account using Get-ADUser." + $result = Get-ADUser -Identity $trimIdentity -Properties * + + } catch { + + Write-Verbose "$logLead : Account named [$Identity] not found using Get-ADUser: $($_.Exception.Message)" + } + + # No normal user account? Check for a gMSA/MSA + if ($null -eq $result) { + + try { + + Write-Verbose "$logLead : Attempting to find account using Get-ADServiceAccount." + $result = Get-ADServiceAccount -Identity $trimIdentity -Properties * + + } catch { + + Write-Verbose "$logLead : Account named [$Identity] not found using Get-ADServiceAccount: $($_.Exception.Message)" + } + } + + # Still nothing? Tough luck kid. Write a warning. + if ($null -eq $result) { + + Write-Warning "$logLead : No account could be located with the supplied account name." + + if (-NOT (Test-IsUserDomainAdmin)) { + + Write-Warning "$logLead : This command is being run without domain administrative privileges. In some cases, elevated permissions may be required to locate accounts." + } + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.tests.ps1 new file mode 100644 index 0000000..46ae135 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-ActiveDirectoryAccount.tests.ps1 @@ -0,0 +1,123 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ActiveDirectoryAccount" { + + $fakeAccountName = "FakeyMcFakeAccount" + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-ActiveDirectoryAccount.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Context "User Permissions" { + + It "Writes a Warning if No Account Found and the User Does Not Have Domain Admin Rights" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { } + + Get-ActiveDirectoryAccount $fakeAccountName | Should -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "This command is being run without domain administrative privileges" } -Times 1 -Exactly -Scope It + } + } + + Context "When Accounts Are Not Found" { + + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { } + + It "Catches Exceptions, Writes to Verbose Stream, and Continues When no AD User Found" { + + $expectedExceptionMessage = "Fuzzy Wuzzy Was A Bear" + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { throw $expectedExceptionMessage } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { } + + { Get-ActiveDirectoryAccount $fakeAccountName -Verbose } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Verbose ` + -ParameterFilter { $Message -match "Get-ADUser: $expectedExceptionMessage" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount -Times 1 -Exactly -Scope It + } + + It "Catches Exceptions, Writes to Verbose Stream, and Continues When no AD Service Account Found" { + + $expectedExceptionMessage = "Fuzzy Wuzzy Had No Hair" + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { throw $expectedExceptionMessage } + + { Get-ActiveDirectoryAccount $fakeAccountName -Verbose } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Verbose ` + -ParameterFilter { $Message -match "Get-ADServiceAccount: $expectedExceptionMessage" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount -Times 1 -Exactly -Scope It + } + } + + Context "Parameter Validation and Manipulation" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + + It "Outputs an object of type PSObject[]" { + + (Get-Command Get-ActiveDirectoryAccount).OutputType.Type.ToString() | Should -BeExactly "System.Management.Automation.PSObject[]" + + } + + It "Writes a Warning and Exits Early if the Identity Contains Only Whitespace Characters" { + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { } + + Get-ActiveDirectoryAccount " " | Should -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Identity \[ \] must contain at least one non-whitespace character." } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount -Times 0 -Exactly -Scope It + } + + It "Does Not Call Get-ADServiceAccount if Get-ADUser Returns User" { + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { return (New-Object Microsoft.ActiveDirectory.Management.ADAccount($fakeAccountName)) } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { } + + Get-ActiveDirectoryAccount $fakeAccountName | Should -Not -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser ` + -ParameterFilter { $Identity.ToString() -eq $fakeAccountName } -Times 1 -Exactly -Scope It + } + + It "Does Call Get-ADServiceAccount if Get-ADUser Returns Null" { + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return (New-Object Microsoft.ActiveDirectory.Management.ADAccount($fakeAccountName)) } + + Get-ActiveDirectoryAccount $fakeAccountName | Should -Not -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser ` + -ParameterFilter { $Identity.ToString() -eq $fakeAccountName } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount ` + -ParameterFilter { $Identity.ToString() -eq $fakeAccountName } -Times 1 -Exactly -Scope It + } + + It "Trims Provided Account Name" { + + Mock Get-ADUser -ModuleName $moduleForMock -MockWith { } + Mock Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return (New-Object Microsoft.ActiveDirectory.Management.ADAccount($fakeAccountName)) } + + Get-ActiveDirectoryAccount " $fakeAccountName " | Should -Not -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADUser ` + -ParameterFilter { $Identity.ToString() -eq $fakeAccountName } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADServiceAccount ` + -ParameterFilter { $Identity.ToString() -eq $fakeAccountName } -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.ps1 new file mode 100644 index 0000000..fda0c7d --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.ps1 @@ -0,0 +1,65 @@ +function Get-BitLockerRecoveryKeys { + + <# + .SYNOPSIS + Returns the BitLocker recovery key for a computer + + .DESCRIPTION + Returns the BitLocker recovery key for a computer. Caller must have domain admin rights + + .PARAMETER HostNames + [string[]] The hostname or array of hostnames to return + + .EXAMPLE + Get-BitLockerRecoveryKey "ALK-DELL1234" + + .EXAMPLE + Get-BitLockerRecoveryKey @("ALK-DELL1234", "ALK-DELL23456") + #> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Parameter(Mandatory)] + [Alias("Computers","ComputerNames")] + [string[]]$HostNames + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-IsUserDomainAdmin)) { + + Write-Warning "$logLead : You must have domain administrative privileges to run this command" + return $null + } + + [array]$hostsToCheck = $HostNames + $recoveryKeys = @() + foreach ($hostName in $hostsToCheck) { + + $computer = Get-ADComputer $hostName -property DistinguishedName + + if ( $null -eq $computer ) { + Write-Warning "$logLead : Unable to find host [$hostName] in AD; verify your hostname." + continue + } + + $bitLockerObject = Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} ` + -SearchBase $computer.DistinguishedName -Properties * + $recoveryKey = $bitLockerObject.'msFVE-RecoveryPassword' + + if ([String]::IsNullOrEmpty($recoveryKey)) { + + Write-Warning "$logLead : Unable to retrieve BitLocker Recovery value for host: [$hostName]" + continue + } + + $recoveryKeys += New-Object PSObject -Property @{ + + HostName = $hostname + RecoveryKey = $recoveryKey + } + } + + return $recoveryKeys +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.tests.ps1 new file mode 100644 index 0000000..0c4a9b0 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-BitLockerRecoveryKeys.tests.ps1 @@ -0,0 +1,58 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-BitLockerRecoveryKeys" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-BitLockerRecoveryKeys.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Context "User Permissions" { + + It "Writes a Warning and Exits Early if the User Does Not Have Domain Admin Rights" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + Mock Get-ADComputer -ModuleName $moduleForMock -MockWith { } + Mock Get-ADObject -ModuleName $moduleForMock -MockWith { } + + Get-BitLockerRecoveryKeys "FAKEHOST123" | Should -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "You must have domain administrative privileges" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADComputer -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADObject -Times 0 -Exactly -Scope It + } + } + + Context "Parameter Validation and Manipulation" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock Get-ADObject -ModuleName $moduleForMock -MockWith { return $null } + + It "Writes a Warning and Continues if the Computer Is Not Found" { + + Mock Get-ADComputer -ModuleName $moduleForMock -MockWith { return $null } + + Get-BitLockerRecoveryKeys "FAKEHOST123" | Should -HaveCount 0 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Unable to find host \[FAKEHOST123\] in AD; verify your hostname." } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADComputer -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADObject -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Continues if the AD Object Is Not Found" { + + Mock Get-ADComputer -ModuleName $moduleForMock -MockWith { return @{ DistinguishedName = 'CN=Test,CN=Managed Service Accounts,DC=foo,DC=bar'} } + + Get-BitLockerRecoveryKeys "FAKEHOST123" | Should -HaveCount 0 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Unable to retrieve BitLocker Recovery value for host: \[FAKEHOST123\]" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADComputer -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ADObject ` + -ParameterFilter { ($SearchBase -match "CN=Test,CN=Managed Service Accounts,DC=foo,DC=bar")} -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.ps1 new file mode 100644 index 0000000..4d20291 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.ps1 @@ -0,0 +1,60 @@ +function Get-DnsByIP { + + <# + .SYNOPSIS + Retrieves all DNS records for a given IP address from Active Directory DNS. + + .DESCRIPTION + Retrieves all DNS records for a given IP address from Active Directory DNS. + + .PARAMETER DNSServer + [string] The DNS server to query. + + .PARAMETER IPAddress + [string] The IP Address to query against. + + .EXAMPLE + Get-DnsByIP -TargetIP 192.168.4.55 -DnsServer 'dc314212.fh.local' + #> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Alias("DomainController")] + [string]$DNSServer = "localhost", + + [Parameter(Mandatory)] + [Alias("TargetIP")] + [string]$IPAddress + ) + + $logLead = (Get-LogLeadName) + + #Verify that the 'Get-DnsServerZone' command is available on the workstation + if ($null -ne (Get-Command -Name Get-DnsServerZone -ErrorAction SilentlyContinue)) { + #Get all of the DNS Zones + $zones = @(Get-DnsServerZone -ComputerName $DNSServer).ZoneName + + #Is the $zones array empty? + if (Test-IsCollectionNullOrEmpty -Collection $zones) { + Write-Host "$logLead : No zones found" + return + } + + #Create an array + $resources = @() + + #Iterate through each zone and add to $resources array if it matches the $IPAddress parameter value + foreach ($zone in $zones) { + + $resources += (Get-DnsServerResourceRecord -ZoneName $zone -ComputerName $DNSServer) | Where-Object {$_.RecordData.IPv4Address.IPAddressToString -eq $IPAddress} + } + + return $resources + } + else { + + Write-Error "$logLead : The command 'Get-DnsServerZone' does not exist on this system. Please verify you are running this on a Domain Controller under an admin accont" + + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.tests.ps1 new file mode 100644 index 0000000..9120de7 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DnsByIP.tests.ps1 @@ -0,0 +1,111 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test Get-DnsByIP" { + + BeforeAll { + #create a module for unavailable commands + $testModule = New-Module -Name DnsServer -ScriptBlock { + function Get-DnsServerZone (){"Get-DnsServerZone"} + function Get-DnsServerResourceRecord (){"Get-DnsServerResourceRecord"} + } + + $testModule | Import-Module + } + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-DnsByIP.tests' } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { return $true } + # Pretend that the Get-DnsServerZone cmdlet is ALWAYS available + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-DnsServerZone -ModuleName $moduleForMock -MockWith {return [pscustomobject]@{"ZoneName" = "TestZone"}} + # Provide a set of fake objects to check against at the end of the function + $mockRecords = { + $records = @() + $records += [PSCustomObject]@{ + Name = 'VALID' + RecordData = @{ + IPv4Address = @{ + IPAddressToString = '192.168.4.3' + } + } + } + $records += [PSCustomObject]@{ + Name = 'NOT-VALID' + RecordData = @{ + IPv4Address = @{ + IPAddressToString = '192.168.4.7' + } + } + } + return $records + } + + Mock -CommandName Get-DnsServerResourceRecord -ModuleName $moduleForMock -MockWith $mockRecords + + Context "Testing" { + + It "Executes properly when Get-Command is not found" { + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $false } + + Get-DnsByIP -TargetIP 192.168.4.55 -DnsServer 'dc314212.fh.local' + + Assert-MockCalled -CommandName Get-Command -Times 1 -Exactly -Scope It + + } + + It "Calls cmdlets the correct amount of times with the -DnsServer parameter" { + + Mock -CommandName Get-Command -ModuleName $moduleForMock -MockWith { return $true } + + Get-DnsByIP -TargetIP 192.168.4.3 -DnsServer 'dc314212.fh.local' + + Assert-MockCalled -CommandName Get-DnsServerResourceRecord -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-DnsServerZone -Times 1 -Exactly -Scope It + + } + + It "Calls cmdlets properly without the -DnsServer parameter" { + + Get-DnsByIP -TargetIP 192.168.4.55 + + Assert-MockCalled -CommandName Get-DnsServerZone -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName "Get-DnsServerResourceRecord" -Times 1 -Exactly -Scope It + + } + + It "Properly finds only the right resource records" { + + $result = Get-DnsByIP -TargetIP 192.168.4.3 + + $result.RecordData.IPv4Address.IPAddressToString | Should -Be '192.168.4.3' + + } + + It "Returns an empty array if no matches are found, remote DNS Server specified" { + $result = Get-DnsByIP -DNSServer "ADC.local" -IPAddress "192.168.4.55" + + $result | Should -Be @() + Assert-MockCalled -CommandName Get-DnsServerZone -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-DnsServerResourceRecord -Times 1 -Exactly -Scope It + + } + + It "Returns an empty array if no matches are found, remote DNS Server not specified" { + $result = Get-DnsByIP -TargetIP "192.168.4.55" + + $result | Should -Be @() + Assert-MockCalled -CommandName Get-DnsServerZone -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-DnsServerResourceRecord -Times 1 -Exactly -Scope It + + } + + + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.ps1 new file mode 100644 index 0000000..c53a1d0 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.ps1 @@ -0,0 +1,46 @@ +function Get-DomainNameDistinguishedName { + + <# + .SYNOPSIS + Splits a domain name string in to distinguished name format. + + .DESCRIPTION + Splits a domain name string in to distinguished name format. I.e. DC=sample,DC=domain,DC=local + + .PARAMETER DomainName + [string] The Domain Name to Split + + .EXAMPLE + Get-DomainNameDistinguishedName "corp.alkamitech.com" -verbose +VERBOSE: [Get-DomainNameDistinguishedName] : Formatting: [corp.alkamitech.com] +VERBOSE: [Get-DomainNameDistinguishedName] : Result: [DC=corp,DC=alkamitech,DC=com] + +DC=corp,DC=alkamitech,DC=com + #> + + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$DomainName + ) + + $logLead = Get-LogLeadName + + if ($DomainName -match "DC=|OU=") { + + Write-Host "$logLead : Domain Name [$DomainName] already appears to be distinguished name format. No transform will occur." + return $DomainName + } + + Write-Verbose "$logLead : Formatting: [$DomainName]" + + # ToDo: Use Join String when we convert to PS7 + # Split on the '.' character, prefix each segment with DC=, then join with ',' + $domainSplitWithPrefix = $DomainName.Split('.') | ForEach-Object { "DC=" + $_ } + $domainNameDistinguishedFormat = ($domainSplitWithPrefix -join ",").TrimEnd(',') + + Write-Verbose "$logLead : Result: [$domainNameDistinguishedFormat]" + return $domainNameDistinguishedFormat +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.tests.ps1 new file mode 100644 index 0000000..415f934 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-DomainNameDistinguishedName.tests.ps1 @@ -0,0 +1,33 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-DomainNameDistinguishedName" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-DomainNameDistinguishedName.tests' } + + Context "Output Validation" { + + It "Handles One-Segment Domain Names" { + + Get-DomainNameDistinguishedName "SomeDomainName" | Should -Be "DC=SomeDomainName" + } + + It "Handles Multi-Segment Domain Names" { + + Get-DomainNameDistinguishedName "Some.Domain.Name" | Should -Be "DC=Some,DC=Domain,DC=Name" + } + } + + Context 'Parameter Validation' { + + It "Returns the Input Parameter if the Parameter is DN Formatted" { + + Get-DomainNameDistinguishedName "DC=Some,DC=Domain,DC=Name" | Should -Be "DC=Some,DC=Domain,DC=Name" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.ps1 new file mode 100644 index 0000000..bb9bdec --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.ps1 @@ -0,0 +1,73 @@ +function Get-SecurityGroupsForUser { + + <# + .SYNOPSIS + Returns security group membership for a user + + .DESCRIPTION + Returns security group membership for a user. Extended security group properties can be accessed from within the Groups property on the return object + + .PARAMETER User + [string] The username to query + + .EXAMPLE + Get-SecurityGroupsForUser "fake.mcfakeuser" + + #> + + [CmdletBinding()] + [OutputType([System.Object[]])] + Param( + [Parameter(Mandatory)] + [Alias("User")] + [string]$UserName + ) + + $logLead = (Get-LogLeadName) + + if ($UserName -match "\\") { + + Write-Verbose "$logLead : Trimming Domain from UserName" + $actualUserName = $UserName.Split("\\") | Select-Object -Last 1 + + } elseif ($UserName -match "@") { + + Write-Verbose "$logLead : Trimming SAMAccountName Suffix from UserName" + $actualUserName = $UserName.Split("@") | Select-Object -First 1 + + } else { + + $actualUserName = $UserName + } + + Write-Host "$logLead : Looking up user information for user: [$actualUserName]" + + $actualUser = Get-ActiveDirectoryAccount -Identity $actualUserName + + if ($null -eq $actualUser) { + + Write-Warning "$logLead : Could not query user details for user: [$actualUserName]" + return $null + } + + $userGroupDNs = $actualUser | Select-Object -ExpandProperty memberOf + + $securityGroups = @() + foreach ($group in $userGroupDNs) { + + $group = (Get-ADGroup $group) + $securityGroup = New-Object PSObject -Property @{ + Name = $group.Name; + SamAccountName = $group.SamAccountName; + DistinguishedName = $group.DistinguishedName; + SID = $group.SID; + Category = $group.GroupCategory; + Scope = $group.GroupScope; + } + + $securityGroup | Add-Member ScriptMethod ToString { $this.Name } -Force + $securityGroups += New-Object PSObject -Property @{ Group = $securityGroup; } + } + + return ($securityGroups | Sort-Object -Property {$_.Group.Name}) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.tests.ps1 new file mode 100644 index 0000000..390af9d --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-SecurityGroupsForUser.tests.ps1 @@ -0,0 +1,41 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-SecurityGroupsForUser" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-SecurityGroupsForUser.tests' } + Mock -CommandName Get-ActiveDirectoryAccount -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + It "Strips off Domain Prefixes for User Lookup" { + + Get-SecurityGroupsForUser "CORP\FakeUser" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ActiveDirectoryAccount ` + -ParameterFilter { $Identity.ToString() -eq "FakeUser"; } -Times 1 -Exactly -Scope It + } + + It "Strips off SAMAccountName Domain Suffixes for User Lookup" { + + Get-SecurityGroupsForUser "FakeUser@corp.alkamitech.com" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ActiveDirectoryAccount ` + -ParameterFilter { $Identity.ToString() -eq "FakeUser"; } -Times 1 -Exactly -Scope It + } + + It "Writes a Warning and Returns Null if the User Is Not Found" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Get-SecurityGroupsForUser "FakeUser" | Should -BeNullOrEmpty + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "Could not query user details for user" } -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.ps1 new file mode 100644 index 0000000..f46daf3 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.ps1 @@ -0,0 +1,272 @@ +function Get-TerminatedComputersReport { + <# + .SYNOPSIS + Generates a report of computers that have not "logged in" in the last n days. + + .DESCRIPTION + Generates a report of computers that have not "logged in" in the last n days. + If an AWS Profile is provided it will also compare against EC2 instances to attempt + to validate a confidence level (0 - 4) of termination, 0 indicating it is almost guaranteed + to be an active system and 4 meaning almost guaranteed to be inactive and "terminated". + If not compared against AWS termination confidence will be 0 or 1. A 1 indicates that + the machine password has not been changed in at least 90 days. + + .PARAMETER Domain + If providing credentials for an alternate domain, specify the domain. + + .PARAMETER Credential + Used when connecting to a non-native domain to generate a report. + + .PARAMETER OU + Optional parameter, specify the search base for AD computer objects. + Example: "OU=FH Computers,DC=FH,DC=local" + + .PARAMETER NumberOfDays + The cut off value for the last logon filter, defaults to 120 days. + + .PARAMETER CheckAWS + Will compare AD to AWS instances for increased accuracy of termination confidence. + + .OUTPUTS + [pscustomobject] + + .EXAMPLE + New-TerminatedComputersReport -Domain fh.local -Credential $credential -OU "OU=POD0,OU=FH Computers,DC=fh,DC=local" -NumberOfDays 90 + + This example retrieves all computer objects in the POD0 OU that have not logged on in preceeding 90 days, a [pscredential] was created and passed in as $credential + .EXAMPLE + New-TerminatedComputersReport -Domain fh.local -Credential $credential -OU "OU=POD0,OU=FH Computers,DC=fh,DC=local" -NumberOfDays 90 -CheckAWS + + This example retrieves all computer objects in the POD0 OU that have not logged on in preceeding 90 days, then retrieves EC2 instance information. + A comparison of Name and IP is done to determine a termination confidence score, which is noted in the output. A [pscredential] was created and passed in as $credential. + .EXAMPLE + New-TerminatedComputersReport -Domain fh.local -Credential $credential -OU "OU=POD0,OU=FH Computers,DC=fh,DC=local" -NumberOfDays 90 -CheckAWS | Export-Csv -Path $env:TEMP\TerminatedComputers.csv -NoTypeInformation + + This example retrieves all computer objects in the POD0 OU that have not logged on in preceeding 90 days, then retrieves EC2 instance information. + It is then piped to Export-Csv for later review. + + + #> + [cmdletbinding(DefaultParameterSetName = "Default")] + param( + [parameter(ParameterSetName = "Credentialed", Mandatory = $true)] + [string] + [ValidateSet("fh.local", "corp.alkamitech.com")] + $Domain, + [parameter(ParameterSetName = "Credentialed", Mandatory = $true)] + [pscredential] + $Credential, + [parameter(ParameterSetName = "Credentialed", Mandatory = $false)] + [parameter(ParameterSetName = "Default", Mandatory = $false)] + [ValidateNotNullorEmpty()] + [string] + $OU, + [parameter(ParameterSetName = "Credentialed", Mandatory = $false)] + [parameter(ParameterSetName = "Default", Mandatory = $false)] + [uint16] + [ValidateRange(1, 365)] + $NumberOfDays = 120, + [parameter(ParameterSetName = "Credentialed", Mandatory = $false)] + [parameter(ParameterSetName = "Default", Mandatory = $false)] + [switch] + $CheckAWS + ) + + $outputObjects = [System.Collections.Generic.List[System.Object]]::new() + $logLead = Get-LogLeadName + + # Make sure AWS Powershell modul is loaded, if needed + if ($PSBoundParameters.ContainsKey("CheckAWS")) { + # Set up some sorting buckets + $ec2Instances = [System.Collections.Generic.List[System.Object]]::new() + $ec2ObjectIndexByIP = [System.Collections.Specialized.OrderedDictionary]::new() + $ec2ObjectIndexByName = [System.Collections.Specialized.OrderedDictionary]::new() + $ec2IPIndex = 0 + $ec2NameIndex = 0 + # Check if this is running in TeamCity, set accounts as appropriate + if ((Test-IsTeamCityProcess)) { + $awsAccounts = "prod", "dev", "qa", "corp" + } + else { + $awsAccounts = "temp-prod", "temp-dev", "temp-qa", "temp-corp" + } + # Make sure AWSPowerShell is loaded/can be loaded + if (!((Get-Module).Name -contains "AWSPowerShell")) { + try { + Import-Module AWSPowerShell + } + catch { + Write-Error "$logLead : Unable to load the AWSPowerShell module" + return + } + } + } + + # If we are specifying credentials we need to make sure we connect to the correct domain for operations + if ($PSBoundParameters.ContainsKey("Credential")) { + try { + $fastDomainController = ((Resolve-DnsName $Domain -ErrorAction Stop).IPAddress | ForEach-Object { Test-Connection $_ -Count 1 -ErrorAction SilentlyContinue } | Sort-Object ResponseTime)[0].Address + } + catch { + Write-Error "$logLead : Error when resolving DNS for $Domain" + return + } + } + + $date = (Get-Date).AddDays(-$NumberofDays) + + # Build a query for AD + $adComputersSplat = @{} + $adComputersSplat += @{"Filter" = { lastLogonDate -lt $date } } + $adComputersSplat += @{"Properties" = @("lastLogonDate", "PasswordLastSet", "Name", "IPv4Address") } + if ($PSBoundParameters.ContainsKey("Credential")) { + $adComputersSplat += @{"Credential" = $Credential } + $adComputersSplat += @{"Server" = $fastDomainController } + } + if ($PSBoundParameters.ContainsKey("OU")) { $adComputersSplat += @{"SearchBase" = $OU } } + # Get AD Computer objects that match our filter + Write-Progress -Activity "Generating Report" -Status "Retrieving AD Objects" -Id 1 -PercentComplete 25 + try { + $adComputers = Get-ADComputer @adComputersSplat + } catch [System.Security.Authentication.AuthenticationException] { + Write-Error "$logLead : Unable to connect to the target domain, invalid credentials." + return + } catch { + Write-Error "$logLead : Unable to retrieve Computer objects from AD." + return + } + + + + if ($PSBoundParameters.ContainsKey("CheckAWS")) { + # Retrieve EC2 data + Write-Progress -Activity "Generating Report" -Status "Retrieving EC2 Data" -Id 1 -PercentComplete 50 + $incrementPercent = 60 + foreach ($awsAccount in $awsAccounts) { + Write-Progress -Activity "Generating Report" -Status "Retrieving EC2 Data from $($awsAccount)" -Id 1 -PercentComplete $incrementPercent + try { + $ec2Instances += (Get-EC2Instance -ProfileName $AWSAccount -Region us-east-1).Instances + } catch { + Write-Error "$logLead : An error occurred while retrieving EC2 instances in us-east-1 for account $($awsAccount)." + } + + if ($awsAccount -like "*prod*") { + try { + $ec2Instances += (Get-EC2Instance -ProfileName $AWSAccount -Region us-west-2).Instances + } catch { + Write-Error "$logLead : An error occurred while retrieving EC2 instances in us-west-2 for account $($awsAccount)." + } + } + $incrementPercent += 5 + } + + # Build some indexes to speed things up later + foreach ($instance in $ec2Instances) { + Write-Progress -Activity "Generating Report" -Status "Retrieving EC2 Data" -Id 1 -PercentComplete ($incrementPercent + 5) + # Index by IPv4 Address + if ($instance.PrivateIpAddress) { + $ec2ObjectIndexByIP.Add($instance.PrivateIpAddress, $ec2IPIndex) + } + $ec2IPIndex++ + } + foreach ($instance in $ec2Instances) { + # Index by the assigned Hostname + if ($instance.tags.key -contains "alk:hostname") { + $name = ($instance.tags | Where-Object { $_.Key -eq "alk:hostname" }).Value.ToLower() + # There are some duplicate hostnames, this will append the index number to avoid throwing an error + if (!($ec2ObjectIndexByName.Contains($name))) { + $ec2ObjectIndexByName.Add($name, $ec2NameIndex) + } + else { + $name = $name + $ec2NameIndex + $ec2ObjectIndexByName.Add($name, $ec2NameIndex) + } + } + $ec2NameIndex++ + } + # Now that we have indexes let's see if we have matching EC2 Instances + foreach ($computer in $adComputers) { + try { + $nameIndex = $ec2ObjectIndexByName[$computer.Name.ToLower()] + } + catch { + $nameIndex = $null + } + try { + $ipIndex = $ec2ObjectIndexByIP[$computer.IPv4Address] + } + catch { + $ipIndex = $null + } + if (!($null -eq $nameIndex)) { + $instanceByName = $ec2Instances[$nameIndex] + } + else { + $instanceByName = $null + } + if (!($null -eq $ipIndex)) { + $instanceByIPv4 = $ec2Instances[$ipIndex] + } + else { + $instanceByIPv4 = $null + } + # Compare results + if (!($null -eq $ipIndex) -or !($null -eq $nameIndex)) { + $ec2NameMatchesIP = (($instanceByName.InstanceId) -eq ($instanceByIPv4.InstanceId)) + } + else { + $ec2NameMatchesIP = $false + } + # If things look to be lining up lets try and get the designation number if it exists + if ($ec2NameMatchesIP -eq $true) { + if ($instanceByName.tags.key -contains "alk:designation") { + $designation = ($instanceByName.tags | Where-Object { $_.Key -eq "alk:designation" }).Value + } + } + else { + $designation = $null + } + # Build a custom object + $tempObj = New-Object -TypeName PSObject + # Store values + $tempObjProps = [ordered]@{ + "ADName" = $computer.Name + "IsEnabled" = $computer.Enabled + "ADIPv4Address" = $computer.IPv4Address + "LastLogon" = $computer.lastLogonDate + "PasswordLastSet" = $computer.PasswordLastSet + "SID" = $computer.SID + "InstanceIdByIP" = ($instanceByIPv4.InstanceId) + "InstanceIdByName" = ($instanceByName.InstanceId) + "DoesInstanceMatch" = $ec2NameMatchesIP + "designation" = $designation + "TerminationConfidence" = [byte](($null -eq $instanceByIPv4) + ($null -eq $instanceByName) + !($ec2NameMatchesIP) + ($computer.PasswordLastSet -lt (Get-Date).AddDays(-90))) + } + # Add custom properties + $tempObj | Add-Member -NotePropertyMembers $tempObjProps -TypeName ComputerObject + $outputObjects.Add($tempObj) + } + } + else { + Write-Progress -Activity "Generating Report" -Id 1 -PercentComplete 95 + foreach ($computer in $adComputers) { + # Build a custom object + $tempObj = New-Object -TypeName PSObject + # Store values + $tempObjProps = [ordered]@{ + "ADName" = $computer.Name + "IsEnabled" = $computer.Enabled + "ADIPv4Address" = $computer.IPv4Address + "LastLogon" = $computer.lastLogonDate + "PasswordLastSet" = $computer.PasswordLastSet + "SID" = $computer.SID + "TerminationConfidence" = [byte]($computer.PasswordLastSet -lt (Get-Date).AddDays(-90)) + } + # Add custom properties + $tempObj | Add-Member -NotePropertyMembers $tempObjProps -TypeName ComputerObject + $outputObjects.Add($tempObj) + } + } + Write-Progress -Activity "Generating Report" -Status "Finished" -Id 1 -Completed + return $outputObjects +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.tests.ps1 new file mode 100644 index 0000000..3c4a215 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-TerminatedComputersReport.tests.ps1 @@ -0,0 +1,201 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +# We need to use their stupid [Amazon.EC2.Model.Tag], so we have to import their module. +Import-Module AWSPowerShell + +Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Get-TerminatedComputersReport.tests' } +Mock -CommandName Write-Progress -ModuleName $moduleForMock -MockWith {} +Mock -CommandName Import-Module -ModuleName $moduleForMock -MockWith {} +Mock -CommandName Get-Date -ModuleName $moduleForMock -MockWith {return ([datetime]::Now)} +Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} +Mock -CommandName Test-IsTeamCityProcess -ModuleName $moduleForMock -MockWith { return $false } + + +$credential = ([pscredential]::new("test",("test1" | ConvertTo-SecureString -AsPlainText -Force))) + +Describe "Get-TerminatedComputersReport" { + Context "When a parameter value is not valid or missing" { + It "Throws an exception if Credential parameter is blank" { + { Get-TerminatedComputersReport -Credential -Domain fh.local } | Should -Throw + } + + It "Throws an exception if Domain parameter is blank" { + { Get-TerminatedComputersReport -Credential $credential -Domain } | Should -Throw + } + + It "Throws an exception if OU is not provided a value" { + { Get-TerminatedComputersReport -OU } | Should -Throw + } + + It "Throws an exception if NumberOfDays is not provided a value" { + { Get-TerminatedComputersReport -NumberOfDays } | Should -Throw + } + + It "Throws an exception if NumberOfDays is outside of the valid range" { + { Get-TerminatedComputersReport -NumberOfDays 366 } | Should -Throw + } + } + + Context "Error Handling"{ + It "Writes an error if AWSPowerShell cannot be loaded" { + Mock -CommandName Get-Module -ModuleName $moduleForMock -MockWith {[pscustomobject]@{"Name" = "NotAWSPowerShell"}} + Mock -CommandName Import-Module -ModuleName $moduleForMock -MockWith { throw "Module not found."} + + Get-TerminatedComputersReport -CheckAWS + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*Unable to load the AWSPowerShell module*"} + } + + It "Writes an error if DNS for the domain cannot be resolved" { + Mock -CommandName Resolve-DnsName -ModuleName $moduleForMock -MockWith { throw "It's always DNS."} + + Get-TerminatedComputersReport -Credential $credential -Domain fh.local + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*Error when resolving DNS for*"} + } + + It "Writes an error if bad credentials are supplied" { + Mock -CommandName Resolve-DnsName -ModuleName $moduleForMock -MockWith { return [pscustomobject]@{"IPAddress"="10.0.0.1"} } + Mock -CommandName Test-Connection -ModuleName $moduleForMock -MockWith { return [pscustomobject]@{"Address"="10.0.0.1";"ResponseTime" = 25} } + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith { throw [System.Security.Authentication.AuthenticationException]::new("The server has rejected the client credentials.") } + + Get-TerminatedComputersReport -Credential $credential -Domain fh.local + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*Unable to connect to the target domain, invalid credentials*"} + } + + It "Writes an error if unable to retrieve AD Computer objects" { + Mock -CommandName Resolve-DnsName -ModuleName $moduleForMock -MockWith { return [pscustomobject]@{"IPAddress"="10.0.0.1"} } + Mock -CommandName Test-Connection -ModuleName $moduleForMock -MockWith { return [pscustomobject]@{"Address"="10.0.0.1";"ResponseTime" = 25} } + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith { throw "Something bad happened"} + + Get-TerminatedComputersReport -Credential $credential -Domain fh.local + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*Unable to retrieve Computer objects from AD*"} + } + + It "Writes an error if unable to retrieve EC2 instances from us-east-1" { + Mock -CommandName Get-Module -ModuleName $moduleForMock -MockWith {[pscustomobject]@{"Name" = "AWSPowerShell"}} + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-EC2Instance -ModuleName $moduleForMock -ParameterFilter { $Region -eq "us-east-1"} -MockWith {throw "Error getting EC2 in us-east-1"} + + Get-TerminatedComputersReport -CheckAWS + + Assert-MockCalled -CommandName Write-Error -Times 4 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*An error occurred while retrieving EC2 instances in us-east-1*"} + } + + It "Writes an error if unable to retrieve EC2 instances from us-west-2"{ + Mock -CommandName Get-Module -ModuleName $moduleForMock -MockWith {[pscustomobject]@{"Name" = "AWSPowerShell"}} + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-EC2Instance -ModuleName $moduleForMock -ParameterFilter { $Region -eq "us-east-1"} -MockWith {} + Mock -CommandName Get-EC2Instance -ModuleName $moduleForMock -ParameterFilter { $Region -eq "us-west-2"} -MockWith {throw "Error getting EC2 in us-west-2"} + + Get-TerminatedComputersReport -CheckAWS + + Assert-MockCalled -CommandName Get-EC2Instance -Times 5 -Exactly -ModuleName $moduleForMock -Scope It + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -ModuleName $moduleForMock -Scope It -ParameterFilter { $Message -like "*An error occurred while retrieving EC2 instances in us-west-2*"} + } + } + + Context "Returns results" { + $testDate = Get-Date + It "Returns results without comparing to AWS" { + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith { + return ( + [pscustomobject]@{ + "Name" = "TestComputerName" + "Enabled" = $true + "IPv4Address" = "10.0.0.2" + "lastLogonDate" = $testDate + "PasswordLastSet" = $testDate + "SID" = "The-Sloth" + } + ) + } + + $adOnlyTest = Get-TerminatedComputersReport + $expected = [pscustomobject]@{ + "ADName" = "TestComputerName" + "IsEnabled" = $true + "ADIPv4Address" = "10.0.0.2" + "LastLogon" = $testDate + "PasswordLastSet" = $testDate + "SID" = "The-Sloth" + "TerminationConfidence" = [byte](($testDate) -lt ($testDate).AddDays(-90)) + } + (($adOnlyTest).ADName -eq ($expected).ADName) | Should -BeTrue + (($adOnlyTest).IsEnabled -eq ($expected).IsEnabled) | Should -BeTrue + (($adOnlyTest).ADIPv4Address -eq ($expected).ADIPv4Address) | Should -BeTrue + (($adOnlyTest).LastLogon -eq ($expected).LastLogon) | Should -BeTrue + (($adOnlyTest).PasswordLastSet -eq ($expected).PasswordLastSet) | Should -BeTrue + (($adOnlyTest).SID -eq ($expected).SID) | Should -BeTrue + (($adOnlyTest).TerminationConfidence -eq ($expected).TerminationConfidence) | Should -BeTrue + } + + It "Returns results comparing to AWS" { + Mock -CommandName Get-Module -ModuleName $moduleForMock -MockWith {[pscustomobject]@{"Name" = "AWSPowerShell"}} + $script:mockCalled = 0 + $getEC2InstanceMock = { + $script:mockCalled++ + if($script:mockCalled -eq 1){ + # This is a hot mess, but we are trying to replicate the blackmagic that AWS has created when returning these objects + return ( + @{"Instances"=[pscustomobject]@{ + "InstanceId" = "i-AmTheSloth" + "PrivateIPAddress" = "10.0.0.2" + "tags" = @([Amazon.EC2.Model.Tag]::new("alk:hostname","TestComputerName"),[Amazon.EC2.Model.Tag]::new("alk:designation","1000")) + } + } + ) + } else { + return $null + } + } + Mock -CommandName Get-ADComputer -ModuleName $moduleForMock -MockWith { + return ( + [pscustomobject]@{ + "Name" = "TestComputerName" + "Enabled" = $true + "IPv4Address" = "10.0.0.2" + "lastLogonDate" = $testDate + "PasswordLastSet" = $testDate + "SID" = "The-Sloth" + } + ) + } + Mock -CommandName Get-EC2Instance -ModuleName $moduleForMock -MockWith $getEC2InstanceMock + + $awsTest = Get-TerminatedComputersReport -CheckAWS + $expected = [pscustomobject]@{ + "ADName" = "TestComputerName" + "IsEnabled" = $true + "ADIPv4Address" = "10.0.0.2" + "LastLogon" = $testDate + "PasswordLastSet" = $testDate + "SID" = "The-Sloth" + "InstanceIdByIP" = "i-AmTheSloth" + "InstanceIdByName" = "i-AmTheSloth" + "DoesInstanceMatch" = $true + "Designation" = "1000" + "TerminationConfidence" = 0 + } + + (($awsTest).ADName -eq ($expected).ADName) | Should -BeTrue + (($awsTest).IsEnabled -eq ($expected).IsEnabled) | Should -BeTrue + (($awsTest).ADIPv4Address -eq ($expected).ADIPv4Address) | Should -BeTrue + (($awsTest).LastLogon -eq ($expected).LastLogon) | Should -BeTrue + (($awsTest).PasswordLastSet -eq ($expected).PasswordLastSet) | Should -BeTrue + (($awsTest).SID -eq ($expected).SID) | Should -BeTrue + (($awsTest).InstanceIdByIP -eq ($expected).InstanceIdByIP) | Should -BeTrue + (($awsTest).InstanceIdByName -eq ($expected).InstanceIdByName) | Should -BeTrue + (($awsTest).DoesInstanceMatch -eq ($expected).DoesInstanceMatch) | Should -BeTrue + (($awsTest).Designation -eq ($expected).Designation) | Should -BeTrue + (($awsTest).TerminationConfidence -eq ($expected).TerminationConfidence) | Should -BeTrue + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.ps1 new file mode 100644 index 0000000..1fa0d64 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.ps1 @@ -0,0 +1,91 @@ +function Get-WorkspaceBundleList { + +<# +.SYNOPSIS + Get a list of AWS Workspace bundles. + +.DESCRIPTION + Get a list of AWS Workspace bundles. This is a convenience wrapper around the AWS PowerShell cmdlet + 'Get-WKSWorkspaceBundle' to provide filtering options needed by the System Engineering team. + +.PARAMETER ProfileName + [string] The AWS profile to use for the operation. If not provided, will default to 'temp-workspaces'. + +.PARAMETER Region + [string] The AWS region to use for the operation. If not provided, will default to 'us-west-2'. + +.PARAMETER Owner + [string] The Workspace bundle owner. If not provided, will default to 'Alkami'. + +.PARAMETER ComputeTypeFilter + [string] Filter to apply to the Workspace bundle list to limit results to a specific compute type. + If not provided, no compute type filtering will be applied. + +.PARAMETER OsFilter + [string] Filter to apply to the Workspace bundle list to limit results to a specific operating system. + If not provided, no operating system type filtering will be applied. Note that this relies on the Name + of the bundle containing the operating system. + +.PARAMETER ProtocolFilter + [string] Filter to apply to the Workspace bundle list to limit results to a specific protocol. + If not provided, no protocol filtering will be applied. Note that this relies on the Description + of the bundle containing the protocol string. + +.EXAMPLE + Get-WorkspaceBundleList +# An array of Alkami bundles. + +.EXAMPLE + Get-WorkspaceBundleList -Owner 'Amazon' -ComputeTypeFilter 'STANDARD' -OsFilter 'Windows 10' -ProtocolFilter 'WorkSpaces Streaming Protocol' +# An array of AWS-produced WSP Standard Windows 10 bundles. +#> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName = 'temp-workspaces', + + [Parameter(Mandatory = $false)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region = 'us-west-2', + + [Parameter(Mandatory = $false)] + [ValidateSet('Alkami', 'Amazon')] + [string] $Owner = 'Alkami', + + [Parameter(Mandatory = $false)] + [ValidateSet('GRAPHICS', 'GRAPHICSPRO', 'PERFORMANCE', 'POWER', 'POWERPRO', 'STANDARD', 'VALUE_TYPE')] + [string] $ComputeTypeFilter = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('Windows 10', 'Amazon Linux 2')] + [string] $OsFilter = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('WorkSpaces Streaming Protocol', 'PCoIP')] + [string] $ProtocolFilter = $null + ) + + Import-AWSModule + + # To filter to Alkami's bundles, we omit the 'Owner' parameter. + $splatParams = @{} + if ( $Owner -eq 'Amazon' ) { + $splatParams["Owner"] = "AMAZON" + } + + # List the bundles using the AWS API. All of the filtering other than Owner must be done after-the-fact because + # AWS decided to give us no capabilities on this API endpoint. + $list = Get-WKSWorkspaceBundle -ProfileName $ProfileName -Region $Region @splatParams + + # Apply any provided filters to the list. + $filteredList = $list | Where-Object { + (( [string]::IsNullOrEmpty( $ComputeTypeFilter ) -or ( $_.ComputeType.Name -eq $ComputeTypeFilter )) -and ` + ( [string]::IsNullOrEmpty( $OsFilter ) -or ( $_.Name -match $OsFilter )) -and ` + ( [string]::IsNullOrEmpty( $ProtocolFilter ) -or ( $_.Description -match $ProtocolFilter ))) + } + + return $filteredList +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.tests.ps1 new file mode 100644 index 0000000..b23e5dd --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Get-WorkspaceBundleList.tests.ps1 @@ -0,0 +1,107 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-WorkspaceBundleList" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { return @( @{ 'Region' = 'us-west-2' } ) } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-WKSWorkspaceBundle -ModuleName $moduleForMock -MockWith { + # Build the first test case. + $test1 = [PSCustomObject] @{ + BundleId = 1 + Name = 'Windows 7' + Description = 'Test' + ComputeType = [PSCustomObject] @{ + Name = 'Power' + } + } + + # Build the second test case. + $test2 = [PSCustomObject]@{ + BundleId = 2 + Name = 'Windows 10' + Description = 'WorkSpaces Streaming Protocol' + ComputeType = [PSCustomObject]@{ + Name = 'GRAPHICS' + } + } + + # Build the third test case. + $test3 = [PSCustomObject]@{ + BundleId = 3 + Name = 'Amazon Linux 2' + Description = 'PCoIP' + ComputeType = [PSCustomObject]@{ + Name = 'PERFORMANCE' + } + } + + return @( $test1, $test2, $test3) + } + + Context "Parameter Validation" { + + It "Throws if ProfileName is Null" { + { Get-WorkspaceBundleList -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName is Empty" { + { Get-WorkspaceBundleList -ProfileName '' } | Should -Throw + } + + It "Throws if Region is Not in Supported List" { + { Get-WorkspaceBundleList -Region 'Test' } | Should -Throw + } + + It "Throws if Owner is Not in Supported List" { + { Get-WorkspaceBundleList -Owner 'Test' } | Should -Throw + } + + It "Throws if ComputeTypeFilter is Not in Supported List" { + { Get-WorkspaceBundleList -ComputeTypeFilter 'Test' } | Should -Throw + } + + It "Throws if OsFilter is Not in Supported List" { + { Get-WorkspaceBundleList -OsFilter 'Test' } | Should -Throw + } + + It "Throws if ProtocolFilter is Not in Supported List" { + { Get-WorkspaceBundleList -ProtocolFilter 'Test' } | Should -Throw + } + } + + Context "Logic Validation" { + + It "Returns All Results With No Filtering By Default" { + + $results = Get-WorkspaceBundleList + $results | Should -HaveCount 3 + } + + It "Applies ComputeTypeFilter If Provided" { + + $results = Get-WorkspaceBundleList -ComputeTypeFilter 'POWER' + $results | Should -HaveCount 1 + $results[0].BundleId | Should -BeExactly 1 + } + + It "Applies OsFilter If Provided" { + + $results = Get-WorkspaceBundleList -OsFilter 'Windows 10' + $results | Should -HaveCount 1 + $results[0].BundleId | Should -BeExactly 2 + } + + It "Applies ProtocolFilter If Provided" { + + $results = Get-WorkspaceBundleList -ProtocolFilter 'PCoIP' + $results | Should -HaveCount 1 + $results[0].BundleId | Should -BeExactly 3 + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.ps1 new file mode 100644 index 0000000..687c61a --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.ps1 @@ -0,0 +1,62 @@ + function Move-AccountToDisabledOU { + + <# + .SYNOPSIS + Moves an AD Account to the Disabled Accounts OU + + .DESCRIPTION + Moves an AD Account to the Disabled Accounts OU + + .PARAMETER AccountDistinguishedName + [string] The DistinguishedName of an AD Account to Act Upon + + .PARAMETER DisabledAccountOU + [string The OU name for disabled accounts. Defaults to "Disabled Accounts" + + .PARAMETER DomainName + [string] The domain name to act upon. Defaults to "fh.local" + + .EXAMPLE + Move-AccountToDisabledOU "fake.serviceaccount") + + .EXAMPLE + Move-AccountToDisabledOU "fake.serviceaccount") -DisabledAccountOU "Trash Can" -Domain "corp.alkamitech.com" + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AccountDistinguishedName, + + [Parameter(Mandatory = $false)] + [string]$DisabledAccountOU = "Disabled Accounts", + + [Parameter(Mandatory = $false)] + [string]$DomainName = "fh.local" + ) + + $logLead = Get-LogLeadName + + if (!(Test-IsUserDomainAdmin)) { + + Write-Warning "$logLead : You must have domain administrative privileges to run this command" + return $nulls + } + + $domainNameDistinguishedName = Get-DomainNameDistinguishedName $DomainName + $disabledAccountOUTrimmed = $DisabledAccountOU.TrimStart("OU=") + $disabledAccountsOUDN = "OU=$disabledAccountOUTrimmed" + $disabledAccountsOUDistinguishedName = "$disabledAccountsOUDN,$domainNameDistinguishedName" + + Write-Host "$logLead : Acting on Account with Distinguished Name [$AccountDistinguishedName]" + if ($AccountDistinguishedName -match $disabledAccountsOUDN) { + + Write-Warning "$logLead : Account is already in Disabled Accounts OU [$disabledAccountsOUDistinguishedName]" + + } else { + + Write-Host "$logLead : Moving account to the Disabled Accounts OU [$disabledAccountsOUDistinguishedName]" + Move-ADObject -Identity $AccountDistinguishedName -TargetPath $disabledAccountsOUDistinguishedName + } + } \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.tests.ps1 new file mode 100644 index 0000000..04c091b --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Move-AccountToDisabledOU.tests.ps1 @@ -0,0 +1,62 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Move-AccountToDisabledOU" { + + $fakeAccountName = "FakeyMcFakeAccount" + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Move-AccountToDisabledOU.tests' } + Mock -CommandName Move-ADObject -ModuleName $moduleForMock -MockWith { } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { } + + Context "User Permissions" { + + It "Writes a Warning and Exits Early if the User Does Not Have Domain Admin Rights" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + + Move-AccountToDisabledOU -AccountDistinguishedName "CN=$fakeAccountName,CN=Managed Service Accounts,OU=Disabled Accounts,DC=fh,DC=local" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "You must have domain administrative privileges" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Move-ADObject -Times 0 -Exactly -Scope It + } + } + + Context "Logic" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + + It "Writes a Warning and Does Not Move the User if it is already in the Disabled Users OU" { + + Move-AccountToDisabledOU -AccountDistinguishedName "CN=$fakeAccountName,CN=Managed Service Accounts,OU=Disabled Accounts,DC=fh,DC=local" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning ` + -ParameterFilter { $Message -match "is already in Disabled Accounts OU" } -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Move-ADObject -Times 0 -Exactly -Scope It + } + + It "Moves the User to the Disabled Users OU" { + + Move-AccountToDisabledOU -AccountDistinguishedName "CN=$fakeAccountName,CN=Managed Service Accounts,OU=Active Accounts,DC=foo,DC=bar" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Move-ADObject -Times 1 -Exactly -Scope It ` + -ParameterFilter { ($Identity -match "$fakeAccountName") -and ($TargetPath -eq "OU=Disabled Accounts,DC=fh,DC=local") } + } + } + + Context "Parameter Validation" { + + Mock Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + + It "Uses the Supplied Domain and OU for the Disabled OU" { + + Move-AccountToDisabledOU -AccountDistinguishedName "CN=$fakeAccountName,CN=Managed Service Accounts,DC=foo,DC=bar" -DisabledAccountOU "Foobar" -DomainName "foo.bar" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Move-ADObject -Times 1 -Exactly -Scope It ` + -ParameterFilter { ($Identity -match "$fakeAccountName") -and ($TargetPath -eq "OU=Foobar,DC=foo,DC=bar") } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.ps1 new file mode 100644 index 0000000..f1f56cb --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.ps1 @@ -0,0 +1,171 @@ +function New-GMSAStack { + +<# +.SYNOPSIS + Creates new GMSA accounts for ORB. + +.DESCRIPTION + Creates new GMSA accounts for ORB. This function creates a new security group (if not pre-existing) and creates a set of gMSA accounts associated with the security group. + Domain Administrator credentials are required to run this function. + +.PARAMETER UserPrefix + [string] The name prefix for the gMSA accounts. Must be alphanumeric not exceeding 5 characters in length (e.g. 'pod99'). + +.PARAMETER TicketNumber + [string] The Jira ticket that is being used for tracking the creation of the gMSA accounts (e.g. 'SYSENG-123456'). + +.PARAMETER TargetEnvironment + [string] The name of the target environment for the gMSA accounts; used to determine the correct pre-existing AD groups for ORB functionality. + +.PARAMETER DomainPostfix + [string] The domain postfix for the domain that the gMSA accounts and the security group will be created in. + +.PARAMETER OUPath + [string] The path where the group account will be created in Active Directory. + +.PARAMETER GroupName + [string] The name of the security group that the gMSA accounts will be associated with. If not existing, a new group will be created. + The group name must be alphanumeric ending in ' - GMSA'. + +.PARAMETER PasswordIntervalDays + [UInt16] The maximum value of days for the password to be valid. Minimum is 30 days; maximum is 365 days. + +.Example + New-GMSAStack -UserPrefix "POD99" -TicketNumber "SYSENG-123456" + +.Example + New-GMSAStack -UserPrefix "POD99" -TicketNumber "SYSENG-123456" -DomainPostfix "domain.local" -OUPath "OU=ServiceAccounts,OU=Prod,OU=SecurityGroups,DC=domain,DC=local" -GroupName "POD99 - GMSA" -PasswordIntervalDays 365 -TargetEnvironment 'Prod' +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-z0-9]{1,5}$")] + [string]$UserPrefix, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-z]+-\d+$")] + [string]$TicketNumber, + + [Parameter(Mandatory = $false)] + [ValidateSet('Dev', 'QA', 'Staging', 'Prod')] + [string]$TargetEnvironment = 'Prod', + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$DomainPostfix = 'fh.local', + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$OUPath = "OU=ServiceAccounts,OU=$TargetEnvironment,OU=SecurityGroups,DC=fh,DC=local", + + [Parameter(Mandatory = $false)] + [ValidatePattern("^[a-z0-9]+ - GMSA$")] + [string]$GroupName = "$UserPrefix - GMSA", + + [Parameter(Mandatory = $false)] + [ValidateRange(30, 365)] + [uint16]$PasswordIntervalDays = 365 + ) + + $logLead = (Get-LogLeadName) + + # Fast fail if user is not a domain admin. + if (!(Test-IsUserDomainAdmin)) { + + Write-Error "$logLead : You must have domain administrative privileges to run this command." + return + } + + # Search for a pre-existing gMSA security group. If not found, attempt to create the group. + $actualGroupName = $GroupName.ToUpperInvariant() + $adGroup = Get-ADGroup -Filter { Name -eq $actualGroupName } + if ( $null -eq $adGroup ) { + + try { + + Write-Host "$logLead : Creating group '$actualGroupName' in '$OUPath'" + $adGroup = (New-ADGroup -Name $actualGroupName -SamAccountName $actualGroupName -GroupCategory Security -GroupScope Global -Path $OUPath -Description $TicketNumber -PassThru) + + } catch { + + Write-Error "$logLead : Creation of group [$actualGroupName] failed. Error encountered was: [$PSItem]" + return + } + + } else { + + Write-Host "$logLead : Found pre-existing group named $actualGroupName; using this group." + } + + # Search for the ORB logs group associated with the target environment. If not found, fail with error. + $orbLogsGroupName = "OrbLogs-$TargetEnvironment" + $orbLogsGroup = Get-ADGroup -Filter { Name -eq $orbLogsGroupName } + if ( $null -eq $orbLogsGroup ) { + + Write-Error "$logLead : Unable to find ORB logs AD group named [$orbLogsGroupName]; verify AD configuration." + return + } + + # Search for the NYDIG SMB access group associated with the target environment. If not found, fail with error. + $nydigAccessGroupName = "NYDIG-SMB-Access-$TargetEnvironment" + $nydigAccessGroup = Get-ADGroup -Filter { Name -eq $nydigAccessGroupName } + if ( $null -eq $nydigAccessGroup ) { + + Write-Error "$logLead : Unable to find NYDIG SMB access AD group named [$nydigAccessGroupName]; verify AD configuration." + return + } + + $listOfAccounts = @( + "$UserPrefix.audit$", + "$UserPrefix.bank$", + "$UserPrefix.content$", + "$UserPrefix.core$", + "$UserPrefix.dbms$", + "$UserPrefix.exception$", + "$UserPrefix.micro$", + "$UserPrefix.msgctr$", + "$UserPrefix.multiplx$", + "$UserPrefix.nag$", + "$UserPrefix.notify$", + "$UserPrefix.radium$", + "$UserPrefix.rpsts$", + "$UserPrefix.schedule$", + "$UserPrefix.secmgr$", + "$UserPrefix.stsconf$" + ) + + Write-Host "$logLead : Creating GMSA accounts in security group $actualGroupName" + foreach ($gmsa in $listOfAccounts) { + + if ( $null -eq (Get-ADServiceAccount -Filter { Name -eq $gmsa }) ) { + + $gmsaFqdn = "$gmsa.$DomainPostfix" + + try { + + Write-Verbose "$logLead : Creating User [$gmsa] as [$gmsaFqdn]" + ( New-ADServiceAccount -name $gmsa -PrincipalsAllowedToRetrieveManagedPassword $adGroup -DNSHostName $gmsaFqdn -Description $TicketNumber -ManagedPasswordIntervalInDays $PasswordIntervalDays ) | Out-Null + + # Add the new account to the ORB logs group. + Add-ADGroupMember -Identity $orbLogsGroup -Members $gmsa + + # Add the micro and DBMS accounts to the NYDIG SMB access group. + if ( $gmsa -match '(micro|dbms)') { + + Add-ADGroupMember -Identity $nydigAccessGroup -Members $gmsa + } + + Write-Host "$logLead : Created [$gmsa]" + + } catch { + + Write-Warning "$logLead : Creation of user [$gmsa] failed. Error encountered was: [$PSItem]" + } + + } else { + + Write-Warning "$logLead : Found pre-existing user named $gmsa; check the configuration of this user." + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.tests.ps1 new file mode 100644 index 0000000..46f015d --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-GMSAStack.tests.ps1 @@ -0,0 +1,334 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-GMSAStack" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'New-GMSAStack.tests' } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Context "Input Validation" { + + It "UserPrefix Should Not Be Empty" { + { New-GMSAStack -UserPrefix '' } | Should -Throw + } + + It "UserPrefix Should Be Less Than 6 Characters In Length" { + { New-GMSAStack -UserPrefix '123456' } | Should -Throw + } + + It "UserPrefix Should Be Alphanumeric" { + { New-GMSAStack -UserPrefix '#@!--' } | Should -Throw + } + + It "TicketNumber Should Not Be Empty" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber '' } | Should -Throw + } + + It "TicketNumber Should Match Regular Expression" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test' } | Should -Throw + } + + It "DomainPostfix Should Not Be Null" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -DomainPostfix $null } | Should -Throw + } + + It "DomainPostfix Should Not Be Empty" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -DomainPostfix '' } | Should -Throw + } + + It "OUPath Should Not Be Null" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -OUPath $null } | Should -Throw + } + + It "OUPath Should Not Be Empty" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -OUPath '' } | Should -Throw + } + + It "GroupName Name Should Not Be Empty" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -GroupName '' } | Should -Throw + } + + It "GroupName Name Should Match Regular Expression" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -GroupName ' - GMSA' } | Should -Throw + } + + It "PasswordIntervalDays Should Be Greater Than 30" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -PasswordIntervalDays 29 } | Should -Throw + } + + It "PasswordIntervalDays Should Be Less Than 365" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -PasswordIntervalDays 366 } | Should -Throw + } + + It "TargetEnvironment Must Be In Allowable List" { + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' -TargetEnvironment 'Test' } | Should -Throw + } + } + + Context "Result Validation" { + + Mock -CommandName Add-ADGroupMember -ModuleName $moduleForMock -MockWith { return $null } + + It "Should Fast Abort With Error If User Is Not Domain Admin" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "You must have domain administrative privileges to run this command" } + } + + It "Should Not Create gMSA Security Group If Group Exists" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Create gMSA Security Group If Group Does Not Exist" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Create gMSA Security Group With Uppercase Name" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Name -eq 'TEST - GMSA' } + } + + It "Should Create gMSA Security Group In OU Path that Reflects Target Environment" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'test' -TicketNumber 'Test-123' -TargetEnvironment 'QA' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Path -ceq 'OU=ServiceAccounts,OU=QA,OU=SecurityGroups,DC=fh,DC=local' } + } + + It "Should Create gMSA Security Group In Production OU Path By Default" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Path -ceq 'OU=ServiceAccounts,OU=Prod,OU=SecurityGroups,DC=fh,DC=local' } + } + + It "Should Abort With Error If gMSA Security Group Creation Fails" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { throw "Test Exception" } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Creation of group \[.*\] failed\. Error encountered was: \[Test Exception\]" } + } + + It "Should Write Error and Not Create Service Accounts If Orb Logs Group Not Found" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Unable to find ORB logs AD group named \[.*\]; verify AD configuration" } + } + + It "Should Write Error and Not Create Service Accounts If NYDIG SMB Access Group Not Found" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { + Switch ( "$Filter" ) + { + { $_ -match 'nydigAccessGroupName' } { $result = $null } + default { $result = New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + } + return $result + } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Unable to find NYDIG SMB access AD group named \[.*\]; verify AD configuration" } + } + + It "Should Write Warnings and Not Create Service Accounts If They Already Exist" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 16 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Found pre-existing user named .*; check the configuration of this user" } + } + + It "Should Create Service Accounts If They Do Not Already Exist" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Should Add gMSA Service Accounts to Pre-Existing AD Groups" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { + Switch ( "$Filter" ) + { + { $_ -match 'orbLogsGroupName' } { $groupName = "OrbLogsGroup" } + { $_ -match 'nydigAccessGroupName' } { $groupName = "NydigAccessGroup" } + default { $groupName = "DefaultGroup" } + } + return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal($groupName) + } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + + Assert-MockCalled -CommandName Add-ADGroupMember -Times 16 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Identity.ToString() -eq 'OrbLogsGroup' } + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Identity.ToString() -eq 'NydigAccessGroup' -and $Members -notmatch '(micro|dbms)' } + Assert-MockCalled -CommandName Add-ADGroupMember -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Identity.ToString() -eq 'NydigAccessGroup' -and $Members -match 'micro' } + Assert-MockCalled -CommandName Add-ADGroupMember -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Identity.ToString() -eq 'NydigAccessGroup' -and $Members -match 'dbms' } + } + + It "Should Write Warning and Continue If Service Account Creation Fails" { + + Mock -CommandName Test-IsUserDomainAdmin -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ADGroup -ModuleName $moduleForMock -MockWith { return New-Object Microsoft.ActiveDirectory.Management.ADPrincipal } + Mock -CommandName New-ADGroup -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Get-ADServiceAccount -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName New-ADServiceAccount -ModuleName $moduleForMock -MockWith { throw "Test Exception" } + + { New-GMSAStack -UserPrefix 'Test' -TicketNumber 'Test-123' } | Should -Not -Throw + + Assert-MockCalled -CommandName Test-IsUserDomainAdmin -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADGroup -Times 3 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADGroup -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName New-ADServiceAccount -Times 16 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Add-ADGroupMember -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 16 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "Creation of user \[.*\] failed\. Error encountered was: \[Test Exception\]" } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.ps1 new file mode 100644 index 0000000..32ec803 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.ps1 @@ -0,0 +1,50 @@ +function New-SecurePassword { +<# +.SYNOPSIS + Generates a secure password. + +.DESCRIPTION + Generates a secure password containing at least one uppercase, one lowercase, one number + and one special character. + +.PARAMETER PasswordLength + [byte] Length of the desired password. + +.PARAMETER ProfileName + [string] The AWS profile to use during password generation. + +.PARAMETER Region + [string] The AWS region to use during password generation. + +.PARAMETER ExcludeCharacters + [string] String containing characters to exclude from the generated password. + +.EXAMPLE + New-SecurePassword -PasswordLength 10 -ProfileName 'temp-dev' -Region 'us-east-1' + +u48d![N[s^ +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $false)] + [ValidateRange(4, 128)] + [byte]$PasswordLength = 15, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $false)] + [string]$ExcludeCharacter = "`"';@`$``%<>=/\" + ) + + # This function is just a convenience wrapper to allow us to default the exclusion list to remove "problematic" + # characters from generated passwords. + Import-AWSModule + return ( Get-SECRandomPassword -ProfileName $ProfileName -Region $Region -RequireEachIncludedType $true -PasswordLength $PasswordLength -ExcludeCharacter $ExcludeCharacter ) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.tests.ps1 new file mode 100644 index 0000000..5ab985d --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SecurePassword.tests.ps1 @@ -0,0 +1,52 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-SecurePassword" { + + Mock -CommandName Get-AWSRegion -ModuleName $moduleForMock -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-SECRandomPassword -ModuleName $moduleForMock -MockWith { return "Ab1!" } + + Context "Parameter Validation" { + + It "Throws if Password Length Is Too Short" { + { New-SecurePassword -PasswordLength 1 -ProfileName 'temp-prod' -Region 'us-east-1' } | Should -Throw + } + + It "Throws if Password Length Is Too Long" { + { New-SecurePassword -PasswordLength 129 -ProfileName 'temp-prod' -Region 'us-east-1'} | Should -Throw + } + + It "Throws if Profile Name is Null" { + { New-SecurePassword -ProfileName $null -Region 'us-east-1' } | Should -Throw + } + + It "Throws if Profile Name is Empty" { + { New-SecurePassword -ProfileName '' -Region 'us-east-1' } | Should -Throw + } + + It "Throws if Region is Not in Supported List" { + { New-SecurePassword -ProfileName 'temp-prod' -Region 'Test' } | Should -Throw + } + } + + Context "Logic" { + + It "Returns a String" { + + (Get-Command New-SecurePassword).OutputType.Type.ToString() | Should -BeExactly "System.String" + } + + It "Proxies Input Parameters to AWS Call" { + New-SecurePassword -PasswordLength 20 -ExcludeCharacter "1" -ProfileName 'temp-prod' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-SECRandomPassword -Times 1 -Exactly -Scope It ` + -ParameterFilter { (( $ProfileName -eq 'temp-prod' ) -and ( $Region -eq 'us-east-1' ) -and ( $PasswordLength -eq 20 ) -and ($ExcludeCharacter -eq '1')) } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.ps1 new file mode 100644 index 0000000..047a362 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.ps1 @@ -0,0 +1,214 @@ +function New-ServerlessServiceAccountPair { +<# +.SYNOPSIS + Top-level function to create serverless service accounts. + +.DESCRIPTION + Top-level function to create serverless service accounts. Initializes data structures and invokes + individual handling functions to create an Active Directory user account pair and an AWS Secrets + Manager secret containing the authentication parameters for the users. + +.PARAMETER Cred + [PSCredential] A credential object for the FH user that has permissions to create new accounts + on the domain. + +.PARAMETER ServiceName + [string] The truncated identifier for the service name. Must be nine characters or less. + +.PARAMETER Environment + [string] The target environment for the user accounts. + +.PARAMETER ProfileName + [string] The AWS profile to use during user creation. + +.PARAMETER TicketNumber + [string] The Jira ticket identifier requesting the new accounts. + +.PARAMETER ServiceAccountIamRoleArn + [string] The AWS IAM role ARN that should be granted access to the AWS Secrets Manager secret. + +.PARAMETER Region + [string] The AWS region to use during user creation. If not provided, defaults to 'us-east-1' + +.PARAMETER ReplicationRegion + [string] The target AWS region for replicated AWS Secrets Manager secrets. + To disable replication, set this value to null or empty. If not provided, defaults to 'us-west-2'. + +.PARAMETER SecretAccessExtraArns + [string[]] An array of AWS ARNs allowed to access the created secret in addition to the defaults. + +.PARAMETER RotationSchedule + [string] A scheduling expression for password rotation. Refer to https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_schedule.html. + To disable rotation, set this value to null or empty. + If not provided, the default scheduling expression will rotate passwords at 1400 UTC on the first Tuesday of the month. + +.PARAMETER RotationWindow + [string] A window duration for password rotation. Refer to https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_schedule.html. + If not provided, the default rotation window is two hours. + +.INPUTS + None. You cannot pipe objects to New-ServerlessServiceAccountPair. + +.OUTPUTS + [PSObject] New-ServerlessServiceAccountPair returns an object with the following top-level members: + [PSCredential[]] Credentials : Contains the usernames and passwords created by this function. + [string[]] SecretArns : Contains the ARN(s) of the AWS Secrets Manager secret(s) created by this function. + +.EXAMPLE + New-ServerlessServiceAccountPair -Cred (Get-AlkamiCredential) ` + -ServiceName 'Example' ` + -Environment 'Prod' ` + -ProfileName 'temp-prod' ` + -TicketNumber 'SYSENG-1234' ` + -ServiceAccountIamRoleArn 'arn:aws:iam::000000000000:role/example-services-accounts-role' ` + -Region 'us-east-1' ` + -ReplicationRegion 'us-west-2' ` + -SecretAccessExtraArns @( 'ExampleArn1' ) + +SecretArns Credentials +---------- ----------- +{ExampleSecretArn1, ExampleSecretArn2} {System.Management.Automation.PSCredential, System.Management.Automation.PSCredential} +#> + [OutputType([PSObject])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [PSCredential] $Cred, + + [Parameter(Mandatory = $true)] + [ValidateLength(1, 8)] + [string] $ServiceName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Dev', 'Qa', 'LoadTest', 'Staging', 'Prod', IgnoreCase = $false)] + [string] $Environment, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-z]+-\d+$")] + [string]$TicketNumber, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ServiceAccountIamRoleArn, + + [Parameter(Mandatory = $false)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region = 'us-east-1', + + [Parameter(Mandatory = $false)] + [ValidateScript({([String]::IsNullOrEmpty($_) -or ($_ -in (Get-AWSRegion).region))})] + [string] $ReplicationRegion = 'us-west-2', + + [Parameter(Mandatory = $false)] + [string[]] $SecretAccessExtraArns = $null, + + [Parameter(Mandatory = $false)] + [string] $RotationSchedule = 'cron(0 14 ? * 3#1 *)', + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $RotationWindow = '2h' + ) + + $logLead = Get-LogLeadName + $usernamePrefix = "${Environment}${ServiceName}Svc" + $environmentTagValue = ( "{0}shared" -f $Environment.ToLower() ) + $userOuPathCommon = "/ServiceAccounts/${Environment}" + $secretName = "${userOuPathCommon}/${usernamePrefix}" + $secretDescription = "Serverless service account secret for $usernamePrefix" + $accessArns = @($ServiceAccountIamRoleArn) + $result = New-Object PSObject -Property @{ + Credentials = @() + SecretArns = @() + } + + # Dump out a list of the decisions we made if the user wants to see them. + Write-Verbose "$logLead : Username prefix evaluated to be '$usernamePrefix'." + Write-Verbose "$logLead : Environment tag evaluated to be '$environmentTagValue'." + Write-Verbose "$logLead : User OU Path that will be used by Active Directory and AWS Secrets code evaluated to be '$userOuPathCommon'." + Write-Verbose "$logLead : Secret name evaluated to be '$secretName'." + Write-Verbose "$logLead : Secret description evaluated to be '$secretDescription'." + + # Generate two usernames and passwords for multi-user rotation strategy. + for ($i = 0; $i -lt 2; $i++) { + + $result.Credentials += ( Get-AlkamiCredential -UserName ( "{0}{1}" -f $usernamePrefix, [char]([byte][char]'A' + $i)) ` + -Password (New-SecurePassword -PasswordLength 20 -ProfileName $ProfileName -Region $Region )) + } + + try { + + # Create users in AD. + New-ServerlessServiceAccountActiveDirectoryUserPair -Cred $Cred ` + -UserDataList $result.Credentials ` + -UserOuPathCommon $userOuPathCommon ` + -Environment $Environment ` + -TicketNumber $TicketNumber + + } catch { + + Write-Error "$logLead : Creation of Active Directory users failed. Error encountered was: [$PSItem]" + return $result + } + + try { + # Create secret + $result.SecretArns = New-ServerlessServiceAccountSecret -SecretName $secretName ` + -UserDataList $result.Credentials ` + -EnvironmentTag $environmentTagValue ` + -ProfileName $ProfileName ` + -Region $Region ` + -ReplicationRegion $ReplicationRegion ` + -Description $secretDescription + + } catch { + + Write-Error "$logLead : Creation of AWS secret failed. Error encountered was: [$PSItem]" + return $result + } + + try { + + New-ServerlessServiceAccountIamPolicy -RoleArn $ServiceAccountIamRoleArn ` + -ProfileName $ProfileName ` + -Region $Region ` + -SecretArns $result.SecretArns + + } catch { + + Write-Warning "$logLead : Creation of IAM policy failed. Error encountered was: [$PSItem]" + } + + try { + $accessArns += $SecretAccessExtraArns + $accessArns += ( Get-YeatsLambdaIamRoleArn -EnvironmentTag $environmentTagValue -ProfileName $ProfileName ) + $accessArns = $accessArns | Where-Object { $false -eq [string]::IsNullOrWhitespace($_) } + + # Create the resource policy on the secret in AWS. + Write-AlkamiSecretResourcePolicy -SecretName $secretName -ProfileName $ProfileName -Region $Region -SecretAccessExtraArns $accessArns + + # Enable rotation on the secret (if the operator didn't turn off that feature). + if ( $false -eq [string]::IsNullOrWhitespace( $RotationSchedule )) { + + $rotationLambdaArn = Get-YeatsRotationLambdaArn -EnvironmentTag $environmentTagValue -ProfileName $ProfileName -Region $Region + if ( $null -ne $rotationLambdaArn ) { + + # Note that we disable immediate rotation of the secret because we just created the accounts -- the passwords + # do not require rotation at this time. + Invoke-SECSecretRotation -SecretId $result.SecretArns[0] -RotateImmediately $false -RotationLambdaArn $rotationLambdaArn ` + -RotationRules_ScheduleExpression $RotationSchedule -RotationRules_Duration $RotationWindow ` + -ProfileName $ProfileName -Region $Region + } + } + + } catch { + + Write-Warning "$logLead : Configuration of AWS secret failed. Error encountered was: [$PSItem]" + } + + return $result +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.tests.ps1 new file mode 100644 index 0000000..431cd2f --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-ServerlessServiceAccountPair.tests.ps1 @@ -0,0 +1,291 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "New-ServerlessServiceAccountPair" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'New-ServerlessServiceAccountPair.tests' } + Mock -CommandName Get-AWSRegion -ModuleName $inScopeModule -MockWith { return @( @{ 'Region' = 'us-east-1' }, @{ 'Region' = 'us-west-2' } ) } + Mock -CommandName New-SecurePassword -ModuleName $inScopeModule -MockWith { return 'TestPassword' } + Mock -CommandName New-ServerlessServiceAccountActiveDirectoryUserPair -ModuleName $inScopeModule -MockWith {} + Mock -CommandName New-ServerlessServiceAccountSecret -ModuleName $inScopeModule -MockWith { return @('TestSecretArn1', 'TestSecretArn2') } + Mock -CommandName New-ServerlessServiceAccountIamPolicy -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Get-YeatsLambdaIamRoleArn -ModuleName $inScopeModule -MockWith { return 'TestYeatsRoleArn' } + Mock -CommandName Get-YeatsRotationLambdaArn -ModuleName $inScopeModule -MockWith { return 'TestYeatsLambdaArn' } + Mock -CommandName Write-AlkamiSecretResourcePolicy -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Invoke-SECSecretRotation -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Write-Error -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Write-Warning -ModuleName $inScopeModule -MockWith {} + + Mock -CommandName Get-AlkamiCredential -ModuleName $inScopeModule -MockWith { + return ( New-Object 'Management.Automation.PsCredential' $UserName, ( ConvertTo-SecureString -AsPlainText -Force -String "$Password" )) + } + + $testCredential = New-Object 'Management.Automation.PsCredential' 'Test', ( ConvertTo-SecureString -AsPlainText -Force -String 'Test' ) + + $validEnvironment = (Get-Command New-ServerlessServiceAccountPair).Parameters.Environment.Attributes.ValidValues[0] + + Context "Parameter Validation" { + + It "Throws if Service Name Length Is Too Short" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName '' } | Should -Throw + } + + It "Throws if Service Name Length Is Too Long" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'ThisIsAServiceThatWeShouldDeployEverywhere' } | Should -Throw + } + + It "Throws if Environment is Not in Supported List" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment 'Test' } | Should -Throw + } + + It "Throws if Environment is in Supported List With Different Casing" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validValue.ToLower() } | Should -Throw + } + + It "Throws if Profile Name is Null" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName $null } | Should -Throw + } + + It "Throws if Profile Name is Empty" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName '' } | Should -Throw + } + + It "Throws if Ticket Number Does Not Match Regex" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test!' } | Should -Throw + } + + It "Throws if Role ARN is Null" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn $null } | Should -Throw + } + + It "Throws if Role ARN is Empty" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn '' } | Should -Throw + } + + It "Throws if Region is Not in Supported List" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'Test' } | Should -Throw + } + + It "Throws if Replication Region is Not in Supported List" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -ReplicationRegion 'Test' } | Should -Throw + } + + It "Throws if Rotation Window is Null" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -RotationWindow $null } | Should -Throw + } + + It "Throws if Rotation Window is Empty" { + { New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -RotationWindow '' } | Should -Throw + } + } + + Context "Logic" { + + It "Creates Two User Passwords" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-SecurePassword -Times 2 -Exactly -Scope It + } + + It "Proxies Provided Arguments to Active Directory Function" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountActiveDirectoryUserPair -Times 1 -Exactly -Scope It ` + -ParameterFilter { (($Cred.UserName -ceq 'Test') -and ($Environment -ceq $validEnvironment) -and ` + ($TicketNumber -ceq 'Test-123')) } + } + + It "Aborts Processing if Active Directory Function Throws" { + + Mock -CommandName New-ServerlessServiceAccountActiveDirectoryUserPair -ModuleName $inScopeModule -MockWith { throw "Test" } + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountActiveDirectoryUserPair -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountSecret -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Creation of Active Directory users failed. Error encountered was' } + + # Revert to top-level mock. + Mock -CommandName New-ServerlessServiceAccountActiveDirectoryUserPair -ModuleName $inScopeModule -MockWith {} + } + + It "Proxies Provided Arguments to AWS Secret Function" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -ReplicationRegion 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountSecret -Times 1 -Exactly -Scope It ` + -ParameterFilter { (($ProfileName -ceq 'temp-test') -and ($Region -ceq 'us-east-1') -and ` + ($ReplicationRegion -ceq 'us-east-1')) } + } + + It "Aborts Processing if Secret Creation Function Throws" { + + Mock -CommandName New-ServerlessServiceAccountSecret -ModuleName $inScopeModule -MockWith { throw "Test" } + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountSecret -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountIamPolicy -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Creation of AWS secret failed. Error encountered was' } + + # Revert mock back to top-level. + Mock -CommandName New-ServerlessServiceAccountSecret -ModuleName $inScopeModule -MockWith { return @('TestSecretArn1', 'TestSecretArn2') } + } + + It "Proxies Provided Arguments to AWS IAM Policy Function" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountIamPolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { (($ProfileName -ceq 'temp-test') -and ($Region -ceq 'us-east-1') -and ($RoleArn -ceq 'TestRoleArn')) } + } + + It "Continues Processing With Warning if AWS IAM Policy Creation Function Throws" { + + Mock -CommandName New-ServerlessServiceAccountIamPolicy -ModuleName $inScopeModule -MockWith { throw "Test" } + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName New-ServerlessServiceAccountIamPolicy -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-YeatsLambdaIamRoleArn -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Creation of IAM policy failed. Error encountered was' } + + # Revert mock back to top-level. + Mock -CommandName New-ServerlessServiceAccountIamPolicy -ModuleName $inScopeModule -MockWith {} + } + + It "Grants Supplied IAM Role Access to Created AWS Secret in Resource Policy" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretAccessExtraArns -contains 'TestRoleArn' } + } + + It "Grants Yeats Lambda IAM Role Access to Created AWS Secret in Resource Policy" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-YeatsLambdaIamRoleArn -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretAccessExtraArns -contains 'TestYeatsRoleArn' } + } + + It "Grants Provided Extra ARNs Access to Created AWS Secret in Resource Policy" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -SecretAccessExtraArns @('TestExtraArn') | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretAccessExtraArns -contains 'TestExtraArn' } + } + + It "Skips Secret Rotation Configuration if User Explicitly Disabled Feature" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' -RotationSchedule $null | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-YeatsRotationLambdaArn -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Invoke-SECSecretRotation -Times 0 -Exactly -Scope It + } + + It "Skips Secret Rotation Configuration if Yeats Rotation Lambda Is Not Found" { + + Mock -CommandName Get-YeatsRotationLambdaArn -ModuleName $inScopeModule -MockWith { return $null } + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-YeatsRotationLambdaArn -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Invoke-SECSecretRotation -Times 0 -Exactly -Scope It + + # Revert mock back to top-level. + Mock -CommandName Get-YeatsRotationLambdaArn -ModuleName $inScopeModule -MockWith { return 'TestYeatsLambdaArn' } + } + + It "Applies Secret Rotation Configuration if Yeats Rotation Lambda Is Found" { + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-YeatsRotationLambdaArn -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Invoke-SECSecretRotation -Times 1 -Exactly -Scope It ` + -ParameterFilter { $RotationLambdaArn -eq 'TestYeatsLambdaArn' } + } + + It "Writes Warning if Configuration of AWS Secret Throws" { + + Mock -CommandName Write-AlkamiSecretResourcePolicy -ModuleName $inScopeModule -MockWith { throw "Test" } + + New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' | Out-Null + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Configuration of AWS secret failed. Error encountered was' } + + # Revert mock back to top-level. + Mock -CommandName Write-AlkamiSecretResourcePolicy -ModuleName $inScopeModule -MockWith {} + } + } + + Context "Output" { + + It "Returns a PSObject" { + + (Get-Command New-ServerlessServiceAccountPair).OutputType.Type.ToString() | Should -BeExactly "System.Management.Automation.PSObject" + } + + It "Returned Object Contains Usernames and Passwords for Created Users" { + + $result = New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' + + $result.Credentials | Should -HaveCount 2 + $result.Credentials[0].UserName | Should -BeExactly "${validEnvironment}TestSvcSvcA" + ( Get-PasswordFromCredential $result.Credentials[0] ) | Should -BeExactly "TestPassword" + $result.Credentials[1].UserName | Should -BeExactly "${validEnvironment}TestSvcSvcB" + ( Get-PasswordFromCredential $result.Credentials[1] ) | Should -BeExactly "TestPassword" + } + + It "Returned Object Contains Secret ARNs" { + + $result = New-ServerlessServiceAccountPair -Cred $testCredential -ServiceName 'TestSvc' -Environment $validEnvironment -ProfileName 'temp-test' ` + -TicketNumber 'Test-123' -ServiceAccountIamRoleArn 'TestRoleArn' -Region 'us-east-1' + + $result.SecretArns | Should -HaveCount 2 + $result.SecretArns[0] | Should -BeExactly "TestSecretArn1" + $result.SecretArns[1] | Should -BeExactly "TestSecretArn2" + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.ps1 new file mode 100644 index 0000000..2dc5a98 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.ps1 @@ -0,0 +1,66 @@ +function New-SftpPasswordHash { +<# +.SYNOPSIS + Generates a password hash for use by SFTP functions. + +.PARAMETER Password + [string] The password to hash. + +.EXAMPLE + New-SftpPasswordHash -Password "Example" +#> + + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Password + ) + + $logLead = Get-LogLeadName + + $scriptBlock = { + + $modulePath = $args[0] + $password = $args[1] + + Import-Module $modulePath + + return (New-PasswordHash $password) + } + + # Primary directory for DLL based on installed module paths. + $dllPath = [IO.Path]::Combine( "$PSScriptRoot", 'Resources', 'EncryptPassword.dll' ) + + if ( $false -eq ( Test-Path $dllPath ) ) { + + # Fallback directory for local testing of this function. + $dllPath = [IO.Path]::Combine( "$PSScriptRoot", '..', 'Resources', 'EncryptPassword.dll' ) + } + + if ( $false -eq ( Test-Path $dllPath ) ) { + + Write-Error "$logLead : Unable to find EncryptPassword.dll! Check your module path and retry." + return $null + } + + # Wrap the call in a PSSession to prevent the DLL from being loaded into our general context. + # If we load the DLL in a new session, the DLL is unloaded when we destroy the session. + # + # Note that wrapping this functionality in an Invoke-Command does not yield a string; it only yields + # an object that behaves like a string unless it is unwrapped by 'ConvertTo-Json'. + # + # To see this for yourself, run the following command: + # ConvertTo-Json ( Invoke-Command -ComputerName localhost -ScriptBlock { return 'Test' }) + # + # To avoid this behavior, we cast the result to a string which appears to trim off those extra properties. + # + # To see this for yourself, run the following command: + # ConvertTo-Json ([string](Invoke-Command -ComputerName localhost -ScriptBlock { return 'Test' })) + $tempSession = New-PSSession + [string]$result = Invoke-Command -Session $tempSession -ScriptBlock $scriptBlock -ArgumentList @( $dllPath, $Password ) + Remove-PSSession -Session $tempSession + + return $result +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.tests.ps1 new file mode 100644 index 0000000..ac78004 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpPasswordHash.tests.ps1 @@ -0,0 +1,32 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-SftpPasswordHash" { + + $fakePassword = "ThisIsAPassword" + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'New-SftpPasswordHash.tests' } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Invoke-Command -ModuleName $moduleForMock -MockWith {} + Mock -CommandName New-PSSession -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Remove-PSSession -ModuleName $moduleForMock -MockWith {} + + Context "Error Handling" { + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $false } + + It "Writes Error and Returns Null If Password Hash DLL Not Found" { + + New-SftpPasswordHash -Password $fakePassword | Should -BeNull + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to find EncryptPassword.dll" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Invoke-Command -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.ps1 new file mode 100644 index 0000000..76f14eb --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.ps1 @@ -0,0 +1,183 @@ +function New-SftpUser { + <# +.SYNOPSIS + Creates a new Alkami SFTP user. + +.DESCRIPTION + Creates a new Alkami SFTP user. Generates the Secrets Manager entry for the user and initializes the SFTP + directory structure. + +.PARAMETER Username + [string] The username of the user to create. + +.PARAMETER Password + [string] The password of the user to create. If not provided, one will be generated. + +.PARAMETER ProfileName + [string] The AWS profile to use during user creation. If not provided, will default to 'temp-prod'. + +.PARAMETER Region + [string] The AWS region to use during user creation. If not provided, will default to 'us-east-1'. + +.PARAMETER HomeDirectorySuffixOverride + [string] Override the default user home directory with a custom relative path. + This option should be used with extreme caution because it can create new users inside + the SFTP folder for another user. The only current use case we have for this override is + for a third-party vendor inside an FI's home directory. + + Valid characters in the path are alphanumeric, '-', and '/'. Valid paths should omit + leading or trailing '/' characters. + +.PARAMETER ChildSubdirectories + [string[]] Array of subdirectories to create under the user's home directory. By default, will + create 'Staging' and 'Production' subdirectories. + + Valid characters in the path are alphanumeric, '-', and '/'. Valid paths should omit + leading or trailing '/' characters. + +.PARAMETER ReplicationRegion + [string] The target AWS region for replicated AWS Secrets Manager secrets. If not provided, will default to 'us-west-2'. + To disable replication, set this value to null or empty. + +.EXAMPLE + New-SftpUser -Username "TestUser-sftp" + +.EXAMPLE + New-SftpUser -Username "TestUser-sftp" -Password "1nsecure-ShouldHaveUsedGenerated!" + +.EXAMPLE + New-SftpUser -Username "TestUser-sftp" -HomeDirectorySuffixOverride "i/probably/should/not/do/this" + +.EXAMPLE + New-SftpUser -Username "TestUser-sftp" -ChildSubdirectories @( 'Subdir1', 'Some/Nested/Subdir' ) + +.EXAMPLE + New-SftpUser -Username "TestUser-sftp" -Password "IHateReplication" -ReplicationRegion '' +#> + + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + [OutputType([PSObject])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Username, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $Password = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('temp-qa', 'temp-prod')] + [string] $ProfileName = 'temp-prod', + + [Parameter(Mandatory = $false)] + [ValidateScript({ $_ -in (Get-SupportedAwsRegions) })] + [string] $Region = 'us-east-1', + + [Parameter(Mandatory = $false)] + [ValidatePattern('^[a-z\d]+(\-[a-z\d]+)*(\/[a-z\d]+(\-[a-z\d]+)*)*$')] + [string] $HomeDirectorySuffixOverride = $null, + + [Parameter(Mandatory = $false)] + [ValidatePattern('^[a-z\d]+(\-[a-z\d]+)*(\/[a-z\d]+(\-[a-z\d]+)*)*$')] + [string[]] $ChildSubdirectories = @( 'Staging', 'Production' ), + + [Parameter(Mandatory = $false)] + [ValidateScript({ ([String]::IsNullOrEmpty($_) -or ($_ -in (Get-SupportedAwsRegions))) })] + [string] $ReplicationRegion = 'us-west-2' + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule + + # Pull the IAM role ARN for the SFTP Authentication Lambda. + # The name is hardcoded in the Terraform where it is created, and we have no convenient way to + # reconcile this name with the Terraform, so assume the name hasn't changed out from under us. + $sftpAuthLambdaRoleArn = (Get-LMFunctionConfiguration -FunctionName 'sftp-auth' -ProfileName $ProfileName -Region $Region).Role + + if ( $false -eq $PSBoundParameters.ContainsKey( 'Password' ) ) { + + Write-Verbose "$logLead : Generating password for user." + $Password = New-SecurePassword -PasswordLength 15 -ProfileName $ProfileName -Region $Region + } + + $actualUsername = $Username.ToLower() + $passwordHash = New-SftpPasswordHash -Password $Password + + if ( $null -eq $passwordHash ) { + + Write-Error "$logLead : Unable to generate password hash for SFTP user." + return $null + } + + if ( $PSBoundParameters.ContainsKey( 'HomeDirectorySuffixOverride' ) -and ` + $PSCmdlet.ShouldProcess( "Override home directory suffix to '$HomeDirectorySuffixOverride", + "Using the HomeDirectorySuffixOverride changes the default behavior of this function. Are you sure you want to use '$HomeDirectorySuffixOverride'?", + 'STOP!' )) { + + $s3HomeDirectorySuffix = $HomeDirectorySuffixOverride + + } else { + + $s3HomeDirectorySuffix = $actualUsername + } + + Write-Verbose "$logLead : S3 home directory suffix determined to be '$s3HomeDirectorySuffix'" + + # Define environmental differences between QA and Production. + if ( $profileName -eq 'temp-prod' ) { + + $s3BucketName = 'alkami-customer-sftp-transfer-server' + $kmsKeyArn = 'arn:aws:kms:us-east-1:790953160341:key/419df36d-c871-4023-b8de-484876f0a1f4' + $transferIamRoleArn = 'arn:aws:iam::790953160341:role/s-822255bad5b544c79-sftp-user-role' + $baseUncPrefix = '\\sftpshare.sre.alkami.net' + + } else { + + $s3BucketName = 'alkami-qa-sftp-transfer-server' + $kmsKeyArn = 'arn:aws:kms:us-east-1:668894625708:key/44112b4d-9671-4786-9864-25fae070b349' + $transferIamRoleArn = 'arn:aws:iam::668894625708:role/s-19391cf5c7d949ca8-sftp-user-role' + $baseUncPrefix = '\\sftpshare.qa.alkami.net' + } + + $uncHomeDirectory = Join-Path (Join-Path $baseUncPrefix $s3BucketName) $s3HomeDirectorySuffix + Write-Verbose "$logLead : UNC home directory string determined to be '$uncHomeDirectory'" + + # Substitute values into the Secrets string. + $secretString = Get-SftpUserDefaultSecretString ` + -BucketName $s3BucketName ` + -HomeDirSuffix $s3HomeDirectorySuffix ` + -KmsArn $kmsKeyArn ` + -RoleArn $transferIamRoleArn ` + -PasswordHash $passwordHash + + # Create the Secrets Manager object in AWS. + $tag1 = New-Object -TypeName PSObject -Property @{ Key = 'alk:project' ; Value = 'sftp' } + $tag2 = New-Object -TypeName PSObject -Property @{ Key = 'alk:costcenter' ; Value = 'sre-systemeng' } + New-SECSecret -SecretString $secretString -Name $actualUsername -Tag @($tag1, $tag2) -ProfileName $ProfileName -Region $Region | Out-Null + + # Create the resource policy on the secret in AWS. + Write-AlkamiSecretResourcePolicy -SecretName $actualUsername -ProfileName $ProfileName -Region $Region -SecretAccessExtraArns @($sftpAuthLambdaRoleArn) + + if ( $false -eq [string]::IsNullOrEmpty( $ReplicationRegion )) { + + $replicaConfig = New-Object Amazon.SecretsManager.Model.ReplicaRegionType + $replicaConfig.Region = $ReplicationRegion + + Write-Verbose "$logLead : Replicating newly created secret to $ReplicationRegion." + Add-SECSecretToRegion -SecretId $actualUsername -AddReplicaRegion @($replicaConfig) -ProfileName $ProfileName -Region $Region | Out-Null + } + + # Create the SMB directories for the user. + Write-Verbose "$logLead : Creating UNC path '$uncHomeDirectory' for the user." + New-Item $uncHomeDirectory -ItemType 'directory' -Force | Out-Null + + foreach ( $subdir in $ChildSubdirectories ) { + $subdirPath = Join-Path $uncHomeDirectory $subdir + Write-Verbose "$logLead : Creating subdirectory UNC path '$subdirPath'." + New-Item $subdirPath -ItemType 'directory' -Force | Out-Null + } + + return New-Object -TypeName PSObject -Property @{ Username = $actualUsername ; Password = $Password } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.tests.ps1 new file mode 100644 index 0000000..a339343 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/New-SftpUser.tests.ps1 @@ -0,0 +1,197 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +InModuleScope 'Alkami.DevOps.SystemEngineering' { + Describe 'New-SftpUser' { + + $fakeAccountName = 'FakeyMcFakeAccount-SFTP' + $fakePassword = 'ThisIsAPassword' + $generatedPassword = '@ut0Generated' + + Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @( 'us-east-1' ) } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'New-SftpUser.tests' } + Mock -CommandName New-SecurePassword -ModuleName $moduleForMock -MockWith { return $generatedPassword } + Mock -CommandName Get-SftpUserDefaultSecretString -ModuleName $moduleForMock -MockWith { return '' } + Mock -CommandName Get-LMFunctionConfiguration -ModuleName $moduleForMock -MockWith { return @{ 'Role' = 'TestArn' } } + Mock -CommandName New-SECSecret -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-AlkamiSecretResourcePolicy -ModuleName $moduleForMock -MockWith {} + Mock -CommandName New-Item -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Add-SECSecretToRegion -ModuleName $moduleForMock -MockWith {} + + Context 'Parameter Validation' { + + It 'Throws if Username is Null' { + { New-SftpUser -Username $null } | Should -Throw + } + + It 'Throws if Username is Empty' { + { New-SftpUser -Username '' } | Should -Throw + } + + It 'Throws if Password is Null' { + { New-SftpUser -Username $fakeAccountName -Password $null } | Should -Throw + } + + It 'Throws if Password is Empty' { + { New-SftpUser -Username $fakeAccountName -Password '' } | Should -Throw + } + + It 'Throws if Profile Name is Null' { + { New-SftpUser -Username $fakeAccountName -ProfileName $null } | Should -Throw + } + + It 'Throws if Profile Name is Empty' { + { New-SftpUser -Username $fakeAccountName -ProfileName '' } | Should -Throw + } + + It 'Throws if Profile Name is Not in Supported List' { + { New-SftpUser -Username $fakeAccountName -ProfileName 'temp-test' } | Should -Throw + } + + It 'Throws if Region is Not in Supported List' { + { New-SftpUser -Username $fakeAccountName -Region 'Test' } | Should -Throw + } + + It 'Throws if Home Directory Suffix Override Does Not Match Regex' { + { New-SftpUser -Username $fakeAccountName -HomeDirectorySuffixOverride '/---/TEST!!' } | Should -Throw + } + + It 'Throws if Child Subdirectories Does Not Match Regex' { + { New-SftpUser -Username $fakeAccountName -ChildSubdirectories @('/---/TEST!!') } | Should -Throw + } + + It 'Throws if Replication Region is Not in Supported List' { + { New-SftpUser -Username $fakeAccountName -ReplicationRegion 'Test' } | Should -Throw + } + } + + Context 'Error Handling' { + + Mock -CommandName New-SftpPasswordHash -ModuleName $moduleForMock -MockWith { return $null } + + It 'Writes Error and Returns Null If Password Hash Fails' { + + New-SftpUser -Username $fakeAccountName | Should -BeNull + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match 'Unable to generate password hash for SFTP user.' } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName New-SECSecret -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName New-Item -Times 0 -Exactly -Scope It + } + } + + Context 'Logic' { + + Mock -CommandName New-SftpPasswordHash -ModuleName $moduleForMock -MockWith { return $fakePassword } + + It 'Converts Username to Lowercase' { + + $result = New-SftpUser -Username $fakeAccountName + $result.Username | Should -BeExactly $fakeAccountName.ToLower() + } + + It 'Uses Password if Provided' { + + $result = New-SftpUser -Username $fakeAccountName -Password $fakePassword + $result.Password | Should -BeExactly $fakePassword + } + + It 'Uses Generated Password if Not Provided' { + + $result = New-SftpUser -Username $fakeAccountName + $result.Password | Should -BeExactly $generatedPassword + } + + It 'Uses Username as Home Directory Suffix By Default' { + + $result = New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName Write-Verbose ` + -ParameterFilter { $Message -match "Home directory suffix determined to be '$($result.Username)'" } -Times 1 -Exactly -Scope It + } + + It 'Uses Home Directory Suffix Override Parameter When Provided' { + + $suffix = 'test1-root/test2-subdir' + New-SftpUser -Username $fakeAccountName -HomeDirectorySuffixOverride $suffix -Confirm:$false + Assert-MockCalled -CommandName Write-Verbose ` + -ParameterFilter { $Message -match "Home directory suffix determined to be '$suffix'" } -Times 1 -Exactly -Scope It + } + + It 'Creates Staging and Production Subdirectories by Default' { + + New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName New-item -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Path.EndsWith('Staging') } + Assert-MockCalled -CommandName New-item -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Path.EndsWith('Production') } + } + + It 'Uses ChildSubdirectories Parameter If Provided' { + + New-SftpUser -Username $fakeAccountName -ChildSubdirectories @( 'TestSubdir' ) + Assert-MockCalled -CommandName New-item -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Path.EndsWith('Staging') } + Assert-MockCalled -CommandName New-item -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Path.EndsWith('Production') } + Assert-MockCalled -CommandName New-item -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Path.EndsWith('TestSubdir') } + } + + It 'Creates AWS Secret for the User' { + + New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName New-SECSecret -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Name -eq $fakeAccountName.ToLower() } + } + + It 'Creates Resource Policy on New AWS Secret' { + + New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName Get-LMFunctionConfiguration -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretName -eq $fakeAccountName.ToLower() } + } + + It 'Resource Policy Contains SFTP Lambda Auth ARN' { + + New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName Get-LMFunctionConfiguration -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-AlkamiSecretResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretAccessExtraArns[0] -ceq 'TestArn' } + } + + It 'Replicates New AWS Secret to us-west-2 by Default' { + + New-SftpUser -Username $fakeAccountName + Assert-MockCalled -CommandName Write-Verbose -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Replicating newly created secret to us-west-2' } + Assert-MockCalled -CommandName Add-SECSecretToRegion -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretId -eq $fakeAccountName.ToLower() } + } + + It 'Does Not Replicate New AWS Secret if Parameter is Null' { + + New-SftpUser -Username $fakeAccountName -ReplicationRegion $null + Assert-MockCalled -CommandName Write-Verbose -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Replicating newly created secret' } + Assert-MockCalled -CommandName Add-SECSecretToRegion -Times 0 -Exactly -Scope It + } + + It 'Does Not Replicate New AWS Secret if Parameter is Empty' { + + New-SftpUser -Username $fakeAccountName -ReplicationRegion '' + Assert-MockCalled -CommandName Write-Verbose -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Message -match 'Replicating newly created secret' } + Assert-MockCalled -CommandName Add-SECSecretToRegion -Times 0 -Exactly -Scope It + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.ps1 new file mode 100644 index 0000000..d6ce6db --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.ps1 @@ -0,0 +1,47 @@ +function Test-IsUserDomainAdmin { + +<# +.SYNOPSIS + Tests if a user is a domain administrator + +.DESCRIPTION + Checks a user's principal group membership for membership in the domain admins group + +.PARAMETER User + [string] The username to check in SAMAccountName format. If not provided, defaults to current user + +.EXAMPLE + Test-IsUserDomainAdmin + +.EXAMPLE + Test-IsUserDomainAdmin "someadmin@corp.alkamitech.com" +#> + + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $false)] + [Alias("UserName")] + [string]$User + ) + + $logLead = (Get-LogLeadName) + + if ([String]::IsNullOrEmpty($User)) { + + $userToCheck = [System.Security.Principal.WindowsIdentity]::GetCurrent() + + } else { + + if ($User -notmatch "\@") { + + Write-Warning "$logLead : Username supplied must be SAMAccountName format" + return $null + } + + $userToCheck = $user + } + + $principal = New-Object System.Security.Principal.WindowsPrincipal($userToCheck) + return $principal.IsInRole("Domain Admins") +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.tests.ps1 new file mode 100644 index 0000000..0005377 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Test-IsUserDomainAdmin.tests.ps1 @@ -0,0 +1,23 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsUserDomainAdmin" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Test-IsUserDomainAdmin.tests' } + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + It "Writes a Warning and Returns Null When Username is Not SAM Format" { + + Test-IsUserDomainAdmin "CONTOSO\FakeUser" | Should -BeNull + Assert-MockCalled -ModuleName $moduleForMock -CommandName "Write-Warning" -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Username supplied must be SAMAccountName format" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.ps1 new file mode 100644 index 0000000..42bfe52 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.ps1 @@ -0,0 +1,119 @@ +function Update-AWSProfile { + +<# +.SYNOPSIS + Update AWS Profile credentials file with temporary assumed role credentials. + +.DESCRIPTION + Update AWS Profile credentials file with temporary assumed role credentials. + +.PARAMETER Profile + [string] The AWS profile name to update. + +.PARAMETER MfaCode + [string] The MFA code from the AWS-associated MFA device. If not provided, will be prompted to enter. + +.PARAMETER SessionDurationSeconds + [uint16] The session duration in seconds for the temporary assumed role. Valid values are 900 seconds (15 minutes) to 43200 seconds (12 hours). + If not provided, will default to 43200. + +.EXAMPLE + Update-AWSProfile -Profile 'Prod' + +.EXAMPLE + Update-AWSProfile -Profile 'Prod' -MfaCode '123456' +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Profile, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$MfaCode = $null, + + [Parameter(Mandatory = $false)] + [ValidateRange(900, 43200)] + [uint16]$SessionDurationSeconds = 43200 + ) + + $logLead = (Get-LogLeadName) + $tempProfile = "temp-" + $Profile.ToLower() + $helpUrl = 'https://confluence.alkami.com/x/hrMHB' + + Import-AWSModule + + try { + + Get-STSCallerIdentity -ProfileName $tempProfile | Out-Null + Write-Verbose "$logLead : Credentials for profile [$tempProfile] are still valid; exiting." + return + + } catch { + + Write-Verbose "$logLead : No valid credentials associated with profile [$tempProfile]; proceeding" + } + + $profileLocation = ( Get-AWSCredential -ListProfileDetail | Where-Object { $_.ProfileName -eq 'default' } | Select-Object -First 1 ).ProfileLocation + if ( [string]::IsNullOrEmpty( $profileLocation ) ) { + + Write-Error "$logLead : Unable to locate default profile location. Check your configuration per [$helpUrl]." + return + } + + $profileCred = Get-AWSCredential -ProfileName $Profile + if ( $null -eq $profileCred ) { + + Write-Error "$logLead : Unable to locate the profile named [$Profile]. Check your configuration per [$helpUrl]." + return + + } elseif ( [string]::IsNullOrEmpty( $profileCred.RoleArn ) ) { + + Write-Error "$logLead : Unable to locate the role ARN for [$Profile]. Check your configuration per [$helpUrl]." + return + + } elseif ( [string]::IsNullOrEmpty( $profileCred.Options.MfaSerialNumber ) ) { + + Write-Error "$logLead : Unable to locate the MFA serial number for [$Profile]. Check your configuration per [$helpUrl]." + return + } + + if ( $false -eq $PSBoundParameters.ContainsKey( 'MfaCode' ) ) { + + $MfaCode = Read-Host -Prompt "Enter MFA code to assume role [$($profileCred.RoleArn)]" + } + + $assumedCred = (Use-STSRole -RoleArn $profileCred.RoleArn -SerialNumber $profileCred.Options.MfaSerialNumber ` + -RoleSessionName $tempProfile -TokenCode $MfaCode -DurationInSeconds $SessionDurationSeconds).Credentials + if ( $null -eq $assumedCred ) { + + Write-Error "$logLead : Unable to assume role [$($profileCred.RoleArn)]. Check your MFA code and retry." + return + + } elseif ( [string]::IsNullOrEmpty( $assumedCred.AccessKeyId ) ) { + + Write-Error "$logLead : No access key provided by [$($profileCred.RoleArn)] credential." + return + + } elseif ( [string]::IsNullOrEmpty( $assumedCred.SecretAccessKey ) ) { + + Write-Error "$logLead : No secret access key provided by [$($profileCred.RoleArn)] credential." + return + + } elseif ( [string]::IsNullOrEmpty( $assumedCred.SessionToken ) ) { + + Write-Error "$logLead : No session token provided by [$($profileCred.RoleArn)] credential." + return + } + + Set-AWSCredential ` + -StoreAs $tempProfile ` + -ProfileLocation $profileLocation ` + -AccessKey $assumedCred.AccessKeyId ` + -SecretKey $assumedCred.SecretAccessKey ` + -SessionToken $assumedCred.SessionToken + + Write-Verbose "$logLead : Updated profile [$tempProfile]." +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.tests.ps1 new file mode 100644 index 0000000..2d330ed --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-AWSProfile.tests.ps1 @@ -0,0 +1,232 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Update-AWSProfile" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Update-AWSProfile.tests' } + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + + Context "Logic" { + + Mock -CommandName Read-Host -ModuleName $moduleForMock -MockWith { return '123456' } + + It "Returns Early If Credential Is Still Valid" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @() } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Get-STSCallerIdentity ` + -ParameterFilter { $ProfileName -eq "temp-test" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Default Profile Location Not Found" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @() } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to locate default profile location" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Profile Not Found" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return $null} ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to locate the profile named \[Test\]" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Profile ARN Not Found" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{}} ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to locate the role ARN for \[Test\]" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Profile MFA Serial Number Not Found" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole" }} ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to locate the MFA serial number for \[Test\]" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Assume Role Fails" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "Unable to assume role \[TestRole\]" } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It + } + + It "Writes Error and Aborts if Assume Role Credential Lacks Access Key" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {@{Credentials = @{}}} + Mock -CommandName Set-AWSCredential -ModuleName $moduleForMock -MockWith {} + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "No access key provided by \[TestRole\] credential." } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-AWSCredential -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Assume Role Credential Lacks Secret Access Key" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {@{Credentials = @{AccessKeyId = "TestAccess"}}} + Mock -CommandName Set-AWSCredential -ModuleName $moduleForMock -MockWith {} + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "No secret access key provided by \[TestRole\] credential." } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-AWSCredential -Times 0 -Exactly -Scope It + } + + It "Writes Error and Aborts if Assume Role Credential Lacks Secret Access Key" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {@{Credentials = @{AccessKeyId = "TestAccess"; SecretAccessKey = "TestSecret"}}} + Mock -CommandName Set-AWSCredential -ModuleName $moduleForMock -MockWith {} + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match "No session token provided by \[TestRole\] credential." } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-AWSCredential -Times 0 -Exactly -Scope It + } + + It "Saves AWS Credential Upon Success" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {@{Credentials = @{AccessKeyId = "TestAccess"; SecretAccessKey = "TestSecret"; SessionToken = "TestSession"}}} + Mock -CommandName Set-AWSCredential -ModuleName $moduleForMock -MockWith {} + + Update-AWSProfile -Profile "Test" + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-AWSCredential -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Set-AWSCredential -Times 1 -Exactly -Scope It + } + } + + Context "Input" { + + Mock -CommandName Get-STSCallerIdentity -ModuleName $moduleForMock -MockWith { throw "Test Error" } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @( @{ProfileLocation = "C:\Temp\test.txt"; ProfileName = "default"}) } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ListProfileDetail' ) } + Mock -CommandName Get-AWSCredential -ModuleName $moduleForMock -MockWith { return @{ RoleArn = "TestRole"; Options = @{ MfaSerialNumber = "TestMfa" } } } ` + -ParameterFilter { $PSBoundParameters.ContainsKey( 'ProfileName' ) } + Mock -CommandName Use-STSRole -ModuleName $moduleForMock -MockWith {@{Credentials = @{AccessKeyId = "TestAccess"; SecretAccessKey = "TestSecret"; SessionToken = "TestSession"}}} + Mock -CommandName Set-AWSCredential -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Read-Host -ModuleName $moduleForMock -MockWith { return '123456' } + + It "Does Not Prompt For MFA Code If Parameter Provided" { + + Update-AWSProfile -Profile "Test" -MfaCode '123456' | Out-Null + + Assert-MockCalled -CommandName Read-Host -Times 0 -Exactly -Scope It + } + + It "Prompts For MFA Code If Parameter Not Provided" { + + Update-AWSProfile -Profile "Test" | Out-Null + + Assert-MockCalled -CommandName Read-Host -Times 1 -Exactly -Scope It + } + + It "Uses Default Value for Session Duration If Parameter Not Provided" { + + Update-AWSProfile -Profile "Test" -MfaCode '123456' | Out-Null + + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It ` + -ParameterFilter { $DurationInSeconds -eq 43200 } + } + + It "Uses Provided Value for Session Duration" { + + Update-AWSProfile -Profile "Test" -MfaCode '123456' -SessionDurationSeconds 4321 | Out-Null + + Assert-MockCalled -CommandName Use-STSRole -Times 1 -Exactly -Scope It ` + -ParameterFilter { $DurationInSeconds -eq 4321 } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.ps1 new file mode 100644 index 0000000..93cc342 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.ps1 @@ -0,0 +1,78 @@ +function Update-SftpPassword { + <# +.SYNOPSIS + Updates the password of an Alkami SFTP user. + +.DESCRIPTION + Updates the password of an Alkami SFTP user by updating the Secrets Manager entry for the user. + +.PARAMETER Username + [string] The username of the user to update. Casing must be an exact match. + +.PARAMETER Password + [string] The new password of the user. If not provided, one will be generated. + +.PARAMETER ProfileName + [string] The AWS profile to use during user modification. If not provided, will default to 'temp-prod'. + +.PARAMETER Region + [string] The AWS region to use during user modification. If not provided, will default to 'us-east-1'. + +.EXAMPLE + Update-SftpPassword -Username "TestUser-sftp" + +.EXAMPLE + Update-SftpPassword -Username "TestUser-sftp" -Password "1nsecure-ShouldHaveUsedGenerated!" +#> + + [CmdletBinding()] + [OutputType([PSObject])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Username, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $Password = $null, + + [Parameter(Mandatory = $false)] + [ValidateSet('temp-qa', 'temp-prod')] + [string] $ProfileName = 'temp-prod', + + [Parameter(Mandatory = $false)] + [ValidateScript({ $_ -in (Get-SupportedAwsRegions) })] + [string] $Region = 'us-east-1' + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule + + if ( $false -eq $PSBoundParameters.ContainsKey( 'Password' ) ) { + + Write-Verbose "$logLead : Generating password for user." + $Password = New-SecurePassword -PasswordLength 15 -ProfileName $ProfileName -Region $Region + } + + $passwordHash = New-SftpPasswordHash -Password $Password + + if ( $null -eq $passwordHash ) { + + Write-Error "$logLead : Unable to generate password hash for SFTP user." + return $null + } + + $secretObject = Get-SECSecretValue -SecretId $Username -ProfileName $ProfileName -Region $Region + if ( $null -eq $secretObject ) { + + Write-Error "$logLead : Unable to retrieve secret for user [$Username] using profile [$ProfileName] and region [$Region]." + return $null + } + + $secret = ConvertFrom-Json $secretObject.SecretString + $secret.Password = $passwordHash + Update-SECSecret -SecretId $Username -SecretString (ConvertTo-Json $secret) -ProfileName $ProfileName -Region $Region | Out-Null + + return New-Object -TypeName PSObject -Property @{ Username = $Username ; Password = $Password } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.tests.ps1 new file mode 100644 index 0000000..9a0cd3d --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Update-SftpPassword.tests.ps1 @@ -0,0 +1,111 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +Describe 'Update-SftpPassword' { + + $fakeAccountName = 'FakeyMcFakeAccount-SFTP' + $fakePassword = 'ThisIsAPassword' + $generatedPassword = '@ut0Generated' + + Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @( 'us-east-1' ) } + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'New-SftpUser.tests' } + Mock -CommandName New-SecurePassword -ModuleName $moduleForMock -MockWith { return $generatedPassword } + Mock -CommandName Update-SECSecret -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName ConvertFrom-Json -ModuleName $moduleForMock -MockWith { return New-Object -TypeName PSObject -Property @{ Password = $fakePassword } } + Mock -CommandName ConvertTo-Json -ModuleName $moduleForMock -MockWith { return '{}' } + Mock -CommandName Import-AWSModule -ModuleName $moduleForMock -MockWith {} + + Context 'Parameter Validation' { + + It 'Throws if Username is Null' { + { Update-SftpPassword -Username $null } | Should -Throw + } + + It 'Throws if Username is Empty' { + { Update-SftpPassword -Username '' } | Should -Throw + } + + It 'Throws if Password is Null' { + { Update-SftpPassword -Username $fakeAccountName -Password $null } | Should -Throw + } + + It 'Throws if Password is Empty' { + { Update-SftpPassword -Username $fakeAccountName -Password '' } | Should -Throw + } + + It 'Throws if Profile Name is Null' { + { Update-SftpPassword -Username $fakeAccountName -ProfileName $null } | Should -Throw + } + + It 'Throws if Profile Name is Empty' { + { Update-SftpPassword -Username $fakeAccountName -ProfileName '' } | Should -Throw + } + + It 'Throws if Profile Name is Not in Supported List' { + { Update-SftpPassword -Username $fakeAccountName -ProfileName 'temp-test' } | Should -Throw + } + + It 'Throws if Region is Not in Supported List' { + { Update-SftpPassword -Username $fakeAccountName -Region 'Test' } | Should -Throw + } + } + + Context 'Error Handling' { + + Mock -CommandName Get-SECSecretValue -ModuleName $moduleForMock -MockWith { return $null } + + It 'Writes Error and Returns Null If Password Hash Fails' { + + Mock -CommandName New-SftpPasswordHash -ModuleName $moduleForMock -MockWith { return $null } + + Update-SftpPassword -Username $fakeAccountName | Should -BeNull + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match 'Unable to generate password hash for SFTP user.' } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Update-SECSecret -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-SECSecretValue -Times 0 -Exactly -Scope It + } + + It 'Writes Error and Returns Null if AWS Lookup Fails' { + + Mock -CommandName New-SftpPasswordHash -ModuleName $moduleForMock -MockWith { return $fakePassword } + + Update-SftpPassword -Username $fakeAccountName | Should -BeNull + + Assert-MockCalled -CommandName Write-Error ` + -ParameterFilter { $Message -match 'Unable to retrieve secret for user' } -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Update-SECSecret -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Get-SECSecretValue -Times 1 -Exactly -Scope It + } + } + + Context 'Logic' { + + Mock -CommandName New-SftpPasswordHash -ModuleName $moduleForMock -MockWith { return $fakePassword } + Mock -CommandName Get-SECSecretValue -ModuleName $moduleForMock -MockWith { return '{}' } + + It 'Does Not Modify Username' { + + $result = Update-SftpPassword -Username $fakeAccountName + $result.Username | Should -BeExactly $fakeAccountName + } + + It 'Uses Password if Provided' { + + $result = Update-SftpPassword -Username $fakeAccountName -Password $fakePassword + $result.Password | Should -BeExactly $fakePassword + } + + It 'Uses Generated Password if Not Provided' { + + $result = Update-SftpPassword -Username $fakeAccountName + $result.Password | Should -BeExactly $generatedPassword + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.ps1 new file mode 100644 index 0000000..c867e85 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.ps1 @@ -0,0 +1,48 @@ +function Write-AlkamiSecretResourcePolicy { +<# +.SYNOPSIS + Creates or overwrites the resource policy for an Alkami AWS Secrets Manager secret. + +.PARAMETER SecretName + [string] The name of the secret to modify. + +.PARAMETER ProfileName + [string] The AWS profile to use during secret modification. + +.PARAMETER Region + [string] The AWS region to use during secret modification. + +.PARAMETER SecretAccessExtraArns + [string[]] An array of AWS ARNs that should be allowed to access the secret in addition to the defaults. + +.EXAMPLE + Write-AlkamiSecretResourcePolicy -SecretName 'Example' -ProfileName 'temp-prod' -Region 'us-east-1' -SecretAccessExtraArns @( 'ExampleArn1', 'ExampleArn2' ) +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SecretName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $ProfileName, + + [Parameter(Mandatory = $true)] + [ValidateScript({$_ -in (Get-AWSRegion).region})] + [string] $Region, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [AllowEmptyCollection()] + [string[]] $SecretAccessExtraArns = $null + ) + + $logLead = Get-LogLeadName + + Import-AWSModule + + Write-Verbose "$logLead : Overwriting resource policy on secret '$SecretName'." + $policyString = Get-AlkamiSecretResourcePolicyString -ProfileName $ProfileName -SecretAccessExtraArns $SecretAccessExtraArns + Write-SECResourcePolicy -SecretId $SecretName -ResourcePolicy $policyString -ProfileName $ProfileName -Region $Region | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.tests.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.tests.ps1 new file mode 100644 index 0000000..a2560f3 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Public/Write-AlkamiSecretResourcePolicy.tests.ps1 @@ -0,0 +1,63 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$global:functionPath = Join-Path -Path $here -ChildPath $sut + +InModuleScope -ModuleName Alkami.DevOps.SystemEngineering -ScriptBlock { + Write-Host "InModuleScope - Overriding SUT: $global:functionPath" + Import-Module $global:functionPath -Force + $inScopeModule = "Alkami.DevOps.SystemEngineering" + + Describe "Write-AlkamiSecretResourcePolicy" { + + Mock -CommandName Get-LogLeadName -ModuleName $inScopeModule -MockWith { return 'Write-AlkamiSecretResourcePolicy.tests' } + Mock -CommandName Get-AWSRegion -ModuleName $inScopeModule -MockWith { return @( @{ 'Region' = 'us-east-1' } ) } + Mock -CommandName Import-AWSModule -ModuleName $inScopeModule -MockWith {} + Mock -CommandName Get-AlkamiSecretResourcePolicyString -ModuleName $inScopeModule -MockWith { return '' } + Mock -CommandName Write-SECResourcePolicy -ModuleName $inScopeModule -MockWith {} + + Context "Parameter Validation" { + + It "Throws if SecretName Is Null" { + { Write-AlkamiSecretResourcePolicy -SecretName $Null } | Should -Throw + } + + It "Throws if SecretName Is Empty" { + { Write-AlkamiSecretResourcePolicy -SecretName '' } | Should -Throw + } + + It "Throws if ProfileName Is Null" { + { Write-AlkamiSecretResourcePolicy -SecretName 'Test' -ProfileName $null } | Should -Throw + } + + It "Throws if ProfileName Is Empty" { + { Write-AlkamiSecretResourcePolicy -SecretName 'Test' -ProfileName '' } | Should -Throw + } + + It "Throws if Region Is Not In Allowable List" { + { Write-AlkamiSecretResourcePolicy -SecretName 'Test' -ProfileName 'temp-test' -Region 'Test' } | Should -Throw + } + } + + Context "Logic" { + + It "Uses Supplied Extra ARNs When Building Resource Policy" { + + Write-AlkamiSecretResourcePolicy -SecretName 'Test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -SecretAccessExtraArns @('TestArn') + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Get-AlkamiSecretResourcePolicyString -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretAccessExtraArns[0] -match "TestArn" } + } + + It "Applies Resource Policy to Secret" { + + Write-AlkamiSecretResourcePolicy -SecretName 'Test' -ProfileName 'temp-test' -Region 'us-east-1' ` + -SecretAccessExtraArns @('TestArn') + + Assert-MockCalled -ModuleName $inScopeModule -CommandName Write-SECResourcePolicy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SecretId -ceq "Test" } + } + } + } +} diff --git a/Modules/Alkami.DevOps.SystemEngineering/Resources/BCrypt.Net-Next.dll b/Modules/Alkami.DevOps.SystemEngineering/Resources/BCrypt.Net-Next.dll new file mode 100644 index 0000000..0e2f5fb Binary files /dev/null and b/Modules/Alkami.DevOps.SystemEngineering/Resources/BCrypt.Net-Next.dll differ diff --git a/Modules/Alkami.DevOps.SystemEngineering/Resources/EncryptPassword.dll b/Modules/Alkami.DevOps.SystemEngineering/Resources/EncryptPassword.dll new file mode 100644 index 0000000..472d875 Binary files /dev/null and b/Modules/Alkami.DevOps.SystemEngineering/Resources/EncryptPassword.dll differ diff --git a/Modules/Alkami.DevOps.SystemEngineering/Resources/return_aws_credentials.ps1 b/Modules/Alkami.DevOps.SystemEngineering/Resources/return_aws_credentials.ps1 new file mode 100644 index 0000000..a80d1c6 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/Resources/return_aws_credentials.ps1 @@ -0,0 +1,105 @@ +# to setup up the credential process, add this contents to ~\.aws\config +<# +[profile temp-] +region = us-east-1 +credential_process =powershell.exe -noprofile C:\ProgramData\chocolatey\lib\Alkami.DevOps.SystemEngineering\module\Resources\return_aws_credentials.ps1 arn:aws:iam::185809956479:mfa/ arn:aws:iam:::role/ +#> +# comment out the same profilename in ~\.aws\credentials +# example for aws dev account +<# +[profile temp-dev] +region = us-east-1 +credential_process =powershell.exe -noprofile C:\ProgramData\chocolatey\lib\Alkami.DevOps.SystemEngineering\module\Resources\return_aws_credentials.ps1 Dev arn:aws:iam::185809:mfa/cookieMonster-cli arn:aws:iam::73722:role/CLI-SRE-ButtonClickers +#> + +param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$MfaDevice, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$ProfileARN +) + +$userProfileName = $env:USERPROFILE +if ($null -ne $userProfileName) { + $credCacheFile = "$($userProfileName)\.aws\$($ProfileName).json" +} elseif ($IsMacOS) { # $IsMacOS is an automatic variable when running on mac + $credCacheFile = "$($HOME)\.aws\$($ProfileName).json" +} + +$tempProfile = "temp-" + $ProfileName.ToLower() + +$tryCount = 0 +$maxTryCount = 3 +$MfaCode = $null + +do { + try { + $cacheJson = Get-Content $credCacheFile -Raw + $cacheObject = ConvertFrom-Json -InputObject $cacheJson + if ([DateTime]$cacheObject.Expiration -gt (Get-Date) ) { + Write-Verbose "[$ProfileName] creds aren't expired" + return $cacheJson + } + } catch { + Write-Verbose "No valid credentials associated with profile [$tempProfile]; proceeding to renew" + } + # only load if we need to actually ask + [system.console]::Beep() # this is a cross platform sound, + $tryCount = $tryCount + 1 + if ($null -ne $userProfileName) { + [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') + $MfaCode = [Microsoft.VisualBasic.Interaction]::InputBox("Enter MFA code for profile '$ProfileName'",'MFA Code','') + } elseif ($IsMacOS) { # $IsMacOS is an automatic variable when running on mac + $macOSMfaCode = "display dialog `"Enter MFA code for profile '$($ProfileName)'`" default answer `"`"" | osascript + # to test locally use this #$macOSMfaCode = 'button returned:ok text returned:8675309' + # splits the string that is returned becuase $macOSMfaCode is a string like this: 'button returned:ok text returned:8675309' + [array]$macOSMfaCodearray = $macOSMfaCode.Split(':') + # Make sure an actual item was returned in the string section of the response + if ($macOSMfaCodearray.Length -ne 3) { + Write-Verbose "Did not get mfa response" + continue + } + # Make sure the string returned is the expected length, this is not the end all test, rather a sanity check + if ($macOSMfaCodearray[2].Length -ne 6) { + Write-Verbose "Did not get correct mfa response" + continue + } + # assign the the mac code to the final code + $mfacode = $macOSMfaCodearray[2] + } + + if ([string]::IsNullOrEmpty($MfaCode) -or $mfacode.Length -ne 6) { + continue + } + $assumedCred = (aws sts assume-role --role-arn $ProfileARN --role-session-name $tempProfile --token-code $MfaCode --serial-number $MfaDevice --duration-seconds 43200 | Out-String | ConvertFrom-Json).Credentials + if ( $null -eq $assumedCred ) { + Write-Error "Unable to assume role [$($ProfileARN)]. Check your MFA code and retry." + } elseif ( [string]::IsNullOrEmpty( $assumedCred.AccessKeyId ) ) { + Write-Error "No access key provided by [$($ProfileARN)] credential." + } elseif ( [string]::IsNullOrEmpty( $assumedCred.SecretAccessKey ) ) { + Write-Error "No secret access key provided by [$($ProfileARN)] credential." + } elseif ( [string]::IsNullOrEmpty( $assumedCred.SessionToken ) ) { + Write-Error "No session token provided by [$($ProfileARN)] credential." + } elseif ( [string]::IsNullOrEmpty( $assumedCred.SessionToken ) ) { + Write-Error "No session token provided by [$($ProfileARN)] credential." + } else { + break + } +} while ( $tryCount -lt $maxTryCount) + +$returnCredJson = ConvertTo-Json -InputObject @{ + Expiration = $assumedCred.Expiration + AccessKeyId = $assumedCred.AccessKeyId + SecretAccessKey = $assumedCred.SecretAccessKey + SessionToken = $assumedCred.SessionToken + Version = 1 +} + +# write to file to cache +Set-Content -Path $credCacheFile -Value $returnCredJson +return $returnCredJson \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..502c875 --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyInstall.ps1 @@ -0,0 +1,44 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + $resourcesFolder = (Join-Path $parentPath "Resources") + if (Test-Path $resourcesFolder) { + + Write-Host "Copying resources folder for module $id to [$targetModuleVersionPath]" + Copy-Item $resourcesFolder -Destination $targetModuleVersionPath -Recurse -Force + } + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..a4a98bf --- /dev/null +++ b/Modules/Alkami.DevOps.SystemEngineering/tools/chocolateyUninstall.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.nuspec b/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.nuspec new file mode 100644 index 0000000..d24b5da --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.nuspec @@ -0,0 +1,26 @@ + + + + Alkami.DevOps.TeamCity + $version$ + Alkami Platform Modules - DevOps - TeamCity + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.TeamCity + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami TeamCity module for use with PowerShell. + + PowerShell + Copyright (c) 2022 Alkami Technologies + + + + + + + + + + diff --git a/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.psd1 b/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.psd1 new file mode 100644 index 0000000..4b53a52 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Alkami.DevOps.TeamCity.psd1 @@ -0,0 +1,11 @@ +@{ + RootModule = 'Alkami.DevOps.TeamCity.psm1' + ModuleVersion = '1.1.0' + GUID = '4a51a6c2-0bcd-45d8-a3b8-e1909d64b022' + Author = 'trowton' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2022 Alkami Technologies, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Get-AuthHeader','Get-BearerToken','Get-DotnetExtraRuntimes','Get-DotnetExtraSDKs','Get-EC2ConsoleSnapshot','Get-EC2InstanceState','Get-EC2WindowsDriverVersions','Get-GitCurrentRelease','Get-TeamCityAgentHostnames','Get-TeamCityEc2Instance','Get-TeamCityHostnames','Get-TeamCityServerNodeHostnames','Invoke-AWSConfigureAWSPackage','Invoke-UpdateChrome','Remove-DotnetExtraRuntimes','Remove-DotnetExtraSDKs','Start-TeamCityAgent','Stop-TeamCityAgent','Test-TeamCityComputerIsAvailable' +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-AuthHeader.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-AuthHeader.ps1 new file mode 100644 index 0000000..ef8ff3f --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-AuthHeader.ps1 @@ -0,0 +1,68 @@ +function Get-AuthHeader { + <# +.SYNOPSIS + Returns an HTTP Authentication/Authorization Header + +.PARAMETER ApiUserUsername + TeamCity User Username to use for building a Basic Auth HTTP Header + +.PARAMETER ApiUserPassword + TeamCity User Password to use for building a Basic Auth HTTP Header + +.PARAMETER BearerToken + A Bearer Token to use for building a Bearer Token Auth HTTP Header + +.PARAMETER GetBearerToken + Call Get-BearerToken function + +.PARAMETER TokenFilePath + Path(optional) to file containing the bearer token to use when calling Get-BearerToken + +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $false)] + [string]$ApiUserUsername = $null, + [Parameter(Mandatory = $false)] + [string]$ApiUserPassword = $null, + [Parameter(Mandatory = $false)] + [string]$BearerToken = $null, + [Parameter(Mandatory = $false)] + [switch]$GetBearerToken, + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$TokenFilePath = $null + ) + $logLead = Get-LogLeadName + + $authHeaderPatternBearer = "Bearer {0}" + if ($GetBearerToken) { + Write-Host "$logLead : Getting token" + $splat = @{} + if ($null -ne $TokenFilePath) { + Write-Host "$logLead : Passing user supplied path - $TokenFilePath" + $splat.TokenFilePath = $TokenFilePath + } + $gotBearerToken = Get-BearerToken @splat + $authHeaderValue = ($authHeaderPatternBearer -f $gotBearerToken) + + } elseif ([string]::IsNullOrEmpty($ApiUserUsername) -or [string]::IsNullOrEmpty($ApiUserPassword)) { + Write-Host "$logLead : Using supplied token" + $authHeaderValue = ($authHeaderPatternBearer -f $BearerToken) + + } else { + Write-Host "$logLead : Using auth pair" + $authorizationPair = "$($apiUserUsername):$($apiUserPassword)" + $encodedCredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($authorizationPair)) + $authHeaderValue = "Basic $encodedCredentials" + + } + + $headers = @{ + Authorization = $authHeaderValue + Accept = "application/json" + } + Write-Host "$logLead : Header creation complete" + return $headers +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-BearerToken.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-BearerToken.ps1 new file mode 100644 index 0000000..f5e9e45 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-BearerToken.ps1 @@ -0,0 +1,27 @@ +function Get-BearerToken { + <# +.SYNOPSIS + Returns a Bearer token from a file + +.PARAMETER TokenFilePath + Path(optional) to file containing the bearer token to use when calling Get-BearerToken +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$TokenFilePath = "~/.teamcity/credentials" + ) + + $logLead = Get-LogLeadName + + if ( -NOT (Test-Path $TokenFilePath)) { + throw "Token file not found - $TokenFilePath" + } + Write-Host "$logLead : Retrieving Bearer Token from $TokenFilePath" + $bearerTokenFile = Get-ChildItem -Path "$TokenFilePath" -File + $bearerToken = Get-Content -Path $bearerTokenFile -Raw + Write-Host "$logLead : Returning Bearer Token" + return $bearerToken +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraRuntimes.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraRuntimes.ps1 new file mode 100644 index 0000000..da43792 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraRuntimes.ps1 @@ -0,0 +1,59 @@ +Function Get-DotnetExtraRuntimes { + <# +.SYNOPSIS + Gets extra runtimes for dotnet. Ensure that we only keep the latest around. + This assumes that minors are as distinct as majors. Supply the flag RemoveNonMajorDupes to get non-latest minors per major. + +.PARAMETER RemoveNonMajorDupes + Only consider majors as "unique" + +.PARAMETER ReturnExpiredRuntimes + Returns the out of date runtimes + +.EXAMPLE + Get-DotnetExtraRuntimes +#> + + [CmdletBinding()] + [OutputType([System.Array])] + param( + [switch]$RemoveNonMajorDupes, + [switch]$ReturnExpiredRuntimes + ) + + $logLead = Get-LogLeadName + $deleteRuntimes = @() + $allRuntimes = @{} + + $runtimes = dotnet --list-runtimes + Write-Host "$logLead : Dotnet Runtimes installed on local machine:" + foreach ($runtime in $runtimes) { + Write-Host "$logLead : $runtime" + $runtimeParentPath = (($runtime -split '\[')[1] -split '\]')[0] + $runtimeApp = ($runtime -split ' ')[0] + $runtimeVersion = [System.Version]($runtime -split ' ')[1] + $runtimePath = (Join-Path $runtimeParentPath $runtimeVersion) + + if ($allRuntimes.Keys -notcontains $runtimeApp) { + Write-Verbose "$logLead : adding version $runtimePath" + $allRuntimes[$runtimeApp] = @{ Version = [System.Version]$runtimeVersion; Path = $runtimePath; }; + } + if ($allRuntimes[$runtimeApp].Version.Major -lt $runtimeVersion.Major) { + } elseif ($allRuntimes[$runtimeApp].Version.Minor -lt $runtimeVersion.Minor -and $RemoveNonMajorDupes) { + Write-Verbose "$logLead : adding delete runtime $($allRuntimes[$runtimeApp].Path)" + $deleteRuntimes += $allRuntimes[$runtimeApp].Path; + } elseif ($allRuntimes[$runtimeApp].Version.Build -lt $runtimeVersion.Build) { + Write-Verbose "$logLead : adding delete runtime $($allRuntimes[$runtimeApp].Path)" + $deleteRuntimes += $allRuntimes[$runtimeApp].Path; + } + $allRuntimes[$runtimeApp] = @{ Version = $runtimeVersion; Path = $runtimePath; }; + } + + Write-Host "$logLead : Dotnet Runtimes that should be deleted/out of date:" + foreach ($deleteRuntime in $deleteRuntimes) { + Write-Host "$logLead : $deleteRuntime" + } + if ($ReturnExpiredRuntimes) { + Return $deleteRuntimes + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraSDKs.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraSDKs.ps1 new file mode 100644 index 0000000..bd38f8a --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-DotnetExtraSDKs.ps1 @@ -0,0 +1,59 @@ +Function Get-DotnetExtraSDKs { + <# +.SYNOPSIS + Delete extra runtimes for dotnet. Ensure that we only keep the latest around. + This assumes that minors are as distinct as majors. Supply the flag RemoveNonMajorDupes to get non-latest minors per major. + +.PARAMETER RemoveNonMajorDupes + Only consider majors as "unique" + +.PARAMETER ReturnExpiredSdks + Returns the out of date SDKs + +.EXAMPLE + Get-DotnetExtraSDKs +#> + + [CmdletBinding()] + [OutputType([System.Array])] + param( + [switch]$RemoveNonMajorDupes, + [switch]$ReturnExpiredSdks + ) + + $logLead = Get-LogLeadName + $deleteSdks = @() + $allSdks = @{} + + $sdks = dotnet --list-sdks + Write-Host "$logLead : Dotnet SDKs installed on local machine:" + foreach ($sdk in $sdks) { + Write-Host "$logLead : $sdk" + $sdkParentPath = (($sdk -split '\[')[1] -split '\]')[0] + $sdkApp = "SDK" + $sdkVersion = [System.Version]($sdk -split ' ')[0] + $sdkPath = (Join-Path $sdkParentPath $sdkVersion) + + if ($allSdks.Keys -notcontains $sdkApp) { + Write-Verbose "$logLead : adding version $sdkPath" + $allSdks[$sdkApp] = @{ Version = [System.Version]$sdkVersion; Path = $sdkPath; }; + } + if ($allSdks[$sdkApp].Version.Major -lt $sdkVersion.Major) { + } elseif ($allSdks[$sdkApp].Version.Minor -lt $sdkVersion.Minor -and $RemoveNonMajorDupes) { + Write-Verbose "$logLead : adding delete sdk $($allSdks[$sdkApp].Path)" + $deleteSdks += $allSdks[$sdkApp].Path; + } elseif ($allSdks[$sdkApp].Version.Build -lt $sdkVersion.Build) { + Write-Verbose "$logLead : adding delete sdk $($allSdks[$sdkApp].Path)" + $deleteSdks += $allSdks[$sdkApp].Path; + } + $allSdks[$sdkApp] = @{ Version = $sdkVersion; Path = $sdkPath; }; + } + + Write-Host "$logLead : Dotnet SDKs that should be deleted/out of date:" + foreach ($deleteSdk in $deleteSdks) { + Write-Host "$logLead : $deleteSdk" + } + if ($ReturnExpiredSdks) { + Return $deleteSdks + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2ConsoleSnapshot.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2ConsoleSnapshot.ps1 new file mode 100644 index 0000000..4149a39 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2ConsoleSnapshot.ps1 @@ -0,0 +1,72 @@ +function Get-EC2ConsoleSnapshot { + <# +.SYNOPSIS + Returns an AWS instance console screenshot .jpg to the user's download folder + +.PARAMETER ComputerName + Hostname(s) of a machine that is located in AWS + +.PARAMETER InstanceId + AWS owned ID(s) of a machine + +.PARAMETER ProfileName + AWS environment credential + +.PARAMETER Type + Optional Parameter. Type of TeamCity host(s) to return: All, Server(s), or Agent(s) +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string[]]$ComputerName, + [Parameter(Mandatory = $false)] + [string[]]$InstanceId, + [Parameter(Mandatory = $true)] + [string]$ProfileName, + [ValidateSet("All", "Server", "Agent")] + [Parameter(Mandatory = $false)] + [string]$Type + ) + $logLead = Get-LogLeadName + + if (!(Test-StringIsNullOrWhitespace -Value $Type)) { + $ComputerName = Get-TeamCityHostnames -Type $Type -ProfileName $ProfileName + } + + if ((Test-IsCollectionNullOrEmpty -Collection $ComputerName) -and (Test-IsCollectionNullOrEmpty -Collection $InstanceId)) { + Write-Warning "$loglead : A ComputerName or InstanceId was not provided, exiting..." + Return + } + + if (Test-IsCollectionNullOrEmpty -Collection $ComputerName) { + $ComputerName = $InstanceId + } + + foreach ($name in $ComputerName) { + if (Test-IsCollectionNullOrEmpty -Collection $InstanceId) { + $hostnameInstanceId = (Get-EC2InstancesByHostname -ProfileName $ProfileName -Servers $name).InstanceId + } else { + $hostnameInstanceId = $name + } + try { + Write-Verbose "$loglead : Attempting Region : us-east-1" + $image = Get-EC2ConsoleScreenshot -InstanceId $hostnameInstanceId -ProfileName $ProfileName -Region "us-east-1" + } catch [InvalidOperationException] { + Write-Verbose "$loglead : Attempting Region : us-west-2" + $image = Get-EC2ConsoleScreenshot -InstanceId $hostnameInstanceId -ProfileName $ProfileName -Region "us-west-2" + } catch { + Write-Host "$($_.Exception.Message)" + } + + $imageData = [Convert]::FromBase64String($image.ImageData) + $datetime = Get-Date -Format "MM_dd_yyyy_HHmm" + $outFile = "$env:USERPROFILE\Downloads\$($name)_$($datetime).jpg" + Write-Host "$loglead : Writing image file to $outFile" + try { + [IO.File]::WriteAllBytes($outFile, $imageData) + } catch { + Write-Error "$logLead : $_" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2InstanceState.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2InstanceState.ps1 new file mode 100644 index 0000000..da8137b --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2InstanceState.ps1 @@ -0,0 +1,66 @@ +function Get-EC2InstanceState { + <# +.SYNOPSIS + Returns an AWS instance status object + +.PARAMETER ComputerName + ComputerName(s) of a machine that is located in AWS + +.PARAMETER InstanceId + AWS owned ID(s) of a machine + +.PARAMETER ProfileName + AWS environment credential + +.PARAMETER Type + Optional Parameter. Type of TeamCity host(s) to return: All, Server(s), or Agent(s) +#> + + [CmdletBinding()] + [OutputType([Object])] + param( + [Parameter(Mandatory = $false)] + [string[]]$ComputerName, + [Parameter(Mandatory = $false)] + [string[]]$InstanceId, + [Parameter(Mandatory = $true)] + [string]$ProfileName, + [ValidateSet("All", "Server", "Agent")] + [Parameter(Mandatory = $false)] + [string]$Type + ) + $endResults = @() + + if (!(Test-StringIsNullOrWhitespace -Value $Type)) { + $ComputerName = Get-TeamCityHostnames -Type $Type -ProfileName $ProfileName + } + + if ((Test-IsCollectionNullOrEmpty -Collection $ComputerName) -and (Test-IsCollectionNullOrEmpty -Collection $InstanceId)) { + Write-Warning "$loglead : A ComputerName or InstanceId was not provided, exiting..." + Return + } + + if (Test-IsCollectionNullOrEmpty -Collection $ComputerName) { + $ComputerName = $InstanceId + } + + foreach ($name in $ComputerName) { + if (Test-IsCollectionNullOrEmpty -Collection $InstanceId) { + $hostnameInstanceId = (Get-EC2InstancesByHostname -ProfileName $ProfileName -Servers $name).InstanceId + }else { + $hostnameInstanceId = $name + } + try { + Write-Verbose "$loglead : Attempting Region : us-east-1" + $results = Get-EC2InstanceStatus -InstanceId $hostnameInstanceId -ProfileName $ProfileName -Region "us-east-1" + $endResults += $results + } catch [InvalidOperationException] { + Write-Verbose "$loglead : Attempting Region : us-west-2" + $results = Get-EC2InstanceStatus -InstanceId $hostnameInstanceId -ProfileName $ProfileName -Region "us-west-2" + $endResults += $results + } catch { + Write-Host "$($_.Exception.Message)" + } + } + Return $endResults +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2WindowsDriverVersions.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2WindowsDriverVersions.ps1 new file mode 100644 index 0000000..d035af4 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-EC2WindowsDriverVersions.ps1 @@ -0,0 +1,26 @@ +function Get-EC2WindowsDriverVersions { + <# +.SYNOPSIS + Get and display the versions of the AWS EC2 Windows Drivers that might need to be updated + #> + [CmdletBinding()] + [OutputType([System.Void])] + #TODO: ADD RetVal of Hashtable of DriverName:DriverVersion or something + + # Where all the windows driver downloads come from + # https://s3.amazonaws.com/ec2-windows-drivers-downloads + $getPVDriverScriptBlock = { + Get-ItemProperty HKLM:\SOFTWARE\Amazon\PVDriver + } + $enaVersionInfoScriptBlock = { + (Get-ChildItem "C:\Windows\System32\drivers\ena.sys").VersionInfo + } + $nvmeVersionInfoScriptBlock = { + (Get-ChildItem "C:\Windows\System32\drivers\AWSNvme.sys").VersionInfo + } + + $servers = Get-TeamCityHostnames + Invoke-Command -ComputerName $servers -ScriptBlock $getPVDriverScriptBlock + Invoke-Command -ComputerName $servers -ScriptBlock $enaVersionInfoScriptBlock + Invoke-Command -ComputerName $servers -ScriptBlock $nvmeVersionInfoScriptBlock +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-GitCurrentRelease.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-GitCurrentRelease.ps1 new file mode 100644 index 0000000..68d0545 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-GitCurrentRelease.ps1 @@ -0,0 +1,80 @@ +Function Get-GitCurrentRelease { + <# +.SYNOPSIS + Get the current latest release version of Git for Windows + +.PARAMETER Uri + Uri of the API endpoint for getting the latest release info + +.PARAMETER DownloadInstaller + Whether to download the 64bit windows installer + +.PARAMETER OutFile + The file to save the downloaded file as. If a full path is not provided, it will be saved in the current working directory. + +.LINK + https://jdhitsolutions.com/blog/powershell/5633/get-git-with-powershell/ +#> + [cmdletbinding()] + Param( + [Parameter(Mandatory = $false, ParameterSetName = "Info")] + [Parameter(Mandatory = $false, ParameterSetName = "Download")] + [ValidateNotNullorEmpty()] + [string]$Uri = "https://api.github.com/repos/git-for-windows/git/releases/latest", + + [Parameter(Mandatory = $false, ParameterSetName = "Download")] + [switch]$DownloadInstaller, + + [Parameter(Mandatory = $true, ParameterSetName = "Download")] + [ValidateNotNullOrEmpty()] + [string]$OutFile + ) + + $logLead = Get-LogLeadName + + Write-Host "$loglead : Getting current release information from $Uri" + try { + $gitForWindows = Invoke-RestMethod -Uri $Uri -Method Get + } catch { + $caughtEx = $_ + Write-Warning "$logLead : Unable to get version data" + Write-Warning "$logLead : $($caughtEx.Exception.Message)" + } + + if ($gitForWindows.tag_name) { + $gitForWindows64Bit = $gitForWindows.Assets.Where({ $_.Name -match "64-bit.exe" }) + $downloadUrl64Bit = $gitForWindows64Bit.browser_download_url + + [pscustomobject]$gitVersionData = @{ + Name = $gitForWindows.name + Version = $gitForWindows.tag_name + Released = $($gitForWindows.published_at -as [datetime]) + DownloadUrl64Bit = $downloadUrl64Bit + } + + if ($PSCmdlet.ParameterSetName -eq "Download") { + $outfileDir = Split-Path -Path $OutFile -Parent + if ([string]::IsNullOrWhiteSpace($outfileDir)) { + Write-Warning "$logLead : NO PATH PROVIDED FOR DOWNLOAD!" + Write-Warning "$logLead : Downloading to Current Working Directory" + $currentWorkingDir = Get-Location + Write-Warning "$logLead : $currentWorkingDir" + } + try { + Write-Host "$logLead : Downloading git for windows`n`tfrom $downloadUrl64Bit`n`tto $Outfile" + Invoke-WebRequest -Uri $downloadUrl64Bit -Method GET -OutFile $OutFile + + } catch { + $StatusCode = $_.Exception.Response.StatusCode.value__ + Write-Warning "$logLead : ERROR DOWNLOADING FILE!" + Write-Warning "$logLead : HTTP Status $StatusCode" + } + } + return $gitVersionData + + + } else { + Write-Host "$logLead : Not found" + return $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityAgentHostnames.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityAgentHostnames.ps1 new file mode 100644 index 0000000..1dd7cd5 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityAgentHostnames.ps1 @@ -0,0 +1,11 @@ +function Get-TeamCityAgentHostnames { + <# +.SYNOPSIS + Returns an array of hostnames of the TeamCity agent instances +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + + return Get-TeamCityHostnames -Type Agent +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityEc2Instance.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityEc2Instance.ps1 new file mode 100644 index 0000000..3e46088 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityEc2Instance.ps1 @@ -0,0 +1,33 @@ +function Get-TeamCityEc2Instance { + <# +.SYNOPSIS + Returns an array of the TeamCity EC2 Instances + +.PARAMETER Type + Type of TeamCity host(s) to return: All, Server(s), or Agent(s) + +.PARAMETER ProfileName + AWS ProfileName +#> + [CmdletBinding()] + [OutputType([Amazon.EC2.Model.Instance[]])] + param( + [Parameter(Mandatory = $false)] + [ValidateSet("All", "Server", "Agent")] + [string]$Type = "All", + [Parameter(Mandatory = $true)] + [string]$ProfileName + ) + + $logLead = Get-LogLeadName + + Write-Host "$logLead : Getting hostnames of type $Type" + $teamcityHostnames = Get-TeamCityHostnames -Type $Type + + Write-Host "$logLead : Getting instances for hostnames $($teamcityHostnames)" + $teamcityInstances = Get-EC2InstancesByHostname -Servers $teamcityHostnames -ProfileName $ProfileName + + Write-Host "$logLead : Found $($teamcityInstances.count)(count) instances to return" + return $teamcityInstances + +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityHostnames.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityHostnames.ps1 new file mode 100644 index 0000000..0c5c03b --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityHostnames.ps1 @@ -0,0 +1,52 @@ +function Get-TeamCityHostnames { + <# +.SYNOPSIS + Returns an array of hostnames of the TeamCity server and agent instances + +.PARAMETER Type + Type of TeamCity host(s) to return: All, Server(s), or Agent(s) +#> + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $false)] + [ValidateSet("All", "Server", "Agent")] + [string]$Type = "All", + [Parameter(Mandatory = $false)] + [string]$ProfileName + ) + + $logLead = Get-LogLeadName + # Because I don't feel like using SharedVariables (yet) + if (Test-StringIsNullOrWhitespace -Value $ProfileName) { + $TeamCityHosts = @{ + All = @("team316179.fh.local", "tea316155.fh.local", "tea31697.fh.local", "tea316229.fh.local", "tea316208.fh.local", "tea46658.fh.local", "tea37021.fh.local", "tea370104.fh.local", "tea37168.fh.local", "tea37079.fh.local", "tea370181.fh.local", "tea370219.fh.local", "tea37020.fh.local", "tea370123.fh.local") + Server = @("team316179.fh.local", "tea37020.fh.local", "tea370123.fh.local") + Agent = @("tea316155.fh.local", "tea31697.fh.local", "tea316229.fh.local", "tea316208.fh.local", "tea46658.fh.local", "tea37021.fh.local", "tea370104.fh.local", "tea37168.fh.local", "tea37079.fh.local", "tea370181.fh.local", "tea370219.fh.local") + } + Write-Host "$logLead : Returning TeamCity Hosts of type - $Type - From all AWS Accounts" + } elseif ($ProfileName -eq "temp-prod") { + $TeamCityHosts = @{ + All = @("team316179.fh.local", "tea316155.fh.local", "tea31697.fh.local", "tea316229.fh.local", "tea316208.fh.local", "tea46658.fh.local") + Server = @("team316179.fh.local") + Agent = @("tea316155.fh.local", "tea31697.fh.local", "tea316229.fh.local", "tea316208.fh.local", "tea46658.fh.local") + } + Write-Host "$logLead : Returning TeamCity Hosts of type - $Type - From the AWS Production Account" + } elseif ($ProfileName -eq "temp-mgmt") { + $TeamCityHosts = @{ + All = @("tea37021.fh.local", "tea370104.fh.local", "tea37168.fh.local", "tea37079.fh.local", "tea370181.fh.local", "tea370219.fh.local", "tea37020.fh.local", "tea370123.fh.local") + Server = @("tea37020.fh.local", "tea370123.fh.local") + Agent = @("tea37021.fh.local", "tea370104.fh.local", "tea37168.fh.local", "tea37079.fh.local", "tea370181.fh.local", "tea370219.fh.local") + } + Write-Host "$logLead : Returning TeamCity Hosts of type - $Type - From the AWS Management Account" + } else { + Write-Warning "ProfileName does not fall under the criteria of temp-prod or temp-mgmt, exiting function..." + return + } + + + + + $hostnamesToReturn = $TeamCityHosts[$Type] + return $hostnamesToReturn +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityServerNodeHostnames.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityServerNodeHostnames.ps1 new file mode 100644 index 0000000..0efa914 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Get-TeamCityServerNodeHostnames.ps1 @@ -0,0 +1,10 @@ +function Get-TeamCityServerNodeHostnames { + <# +.SYNOPSIS + Returns an array of hostnames of the TeamCity agent instances +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + return Get-TeamCityHostnames -Type Server +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Invoke-AWSConfigureAWSPackage.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Invoke-AWSConfigureAWSPackage.ps1 new file mode 100644 index 0000000..aae39d3 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Invoke-AWSConfigureAWSPackage.ps1 @@ -0,0 +1,156 @@ +function Invoke-AWSConfigureAWSPackage { + <# +.SYNOPSIS + Used to help facilitate updating various driver packages on TeamCity agents + +.PARAMETER JobName + Used to specify which driver set to update + +.PARAMETER InstanceId + One or more instance ids to operate on. Defaults to the TC agent machine list + +.PARAMETER Comment + Please provide a Jira ticket number associated with the work you are doing + +.PARAMETER ProfileName + The AWS Profile name to use (think temp-prod, unless this is implemented as a job on TC itself, then Prod, etc) + +.PARAMETER Region + The AWS Region where the command should be run + +.LINK + https://confluence.alkami.com/display/SRE/How+To+update+AWS+EC2+Drivers+on+TeamCity+Agents +#> + [CmdletBinding()] + [OutputType([System.Void])] + param ( + [Parameter(Mandatory = $true)] + [ValidateSet('AwsEnaNetworkDriver', 'AWSPVDriver', 'AWSNVMe')] + [ArgumentCompleter( { $possibleValues = @('AwsEnaNetworkDriver', 'AWSPVDriver', 'AWSNVMe'); return $possibleValues | ForEach-Object { $_ } })] + [string]$JobName, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string[]]$InstanceId = @("i-05993777a46817dfe", "i-0117159b672f24a1a", "i-01c107a3e148d503a", "i-0cc56f29481ad409b", "i-0dfbf5cf56d898966", "i-0b17585457e090315"), + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('JiraTicketNumber')] + [string]$Comment, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ProfileName, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Region + ) + + $logLead = Get-LogLeadName + + $targets = @( + @{ + Key = "InstanceIds" + Values = @($instanceId) #this value has to be an array even if only one value is given, I think + } + ) + # The requirements on the AWS CLI are very odd. I must be doing something wrong + $targetsString = '"' + (ConvertTo-Json $targets -Compress).Replace('"', '""') + '"' + + $parameters = @{ + action = @("Install") + installationType = @("Uninstall and reinstall") + version = @("") + additionalArguments = @("{}") + name = @($JobName) + } + $parametersString = '"' + (ConvertTo-Json $parameters -Compress).Replace('"', '""') + '"' + + Write-Host $targetsString + Write-Host $parametersString + + Write-Host "$logLead : Sending this command to AWS CLI:`n`n aws ssm send-command --document-name `"AWS-ConfigureAWSPackage`" --document-version `"1`" --targets=$targetsString --parameters=$parametersString --comment `"$Comment`" --timeout-seconds `"600`" --max-concurrency `"50`" --max-errors `"0`" --output-s3-bucket-name `"teamcity-alkami-bucket`" --region `"$Region`" --profile `"$ProfileName`"`n`n" + + # https://docs.aws.amazon.com/cli/latest/reference/ssm/send-command.html + $resultString = (aws ssm send-command --document-name "AWS-ConfigureAWSPackage" --document-version "1" --targets=$targetsString --parameters=$parametersString --comment $Comment --timeout-seconds "600" --max-concurrency "50" --max-errors "0" --output-s3-bucket-name "teamcity-alkami-bucket" --region $Region --profile $ProfileName) + + $result = ConvertFrom-Json $resultString -Depth 100 + + $command = $result.Command + + if ($command.ErroCount -gt 0) { + Write-Host "$logLead : response was the following:`n$resultString" + throw "$logLead : Result had non-zero error count. Please review the logs above." + } + + $commandId = $command.CommandId + $instanceIds = $command.Targets[0].Values + + <# + Potential status fields https://docs.aws.amazon.com/cli/latest/reference/ssm/get-command-invocation.html + Pending: The command hasn't been sent to the managed node. + In Progress: The command has been sent to the managed node but hasn't reached a terminal state. + Delayed: The system attempted to send the command to the target, but the target wasn't available. The managed node might not be available because of network issues, because the node was stopped, or for similar reasons. The system will try to send the command again. + Success: The command or plugin ran successfully. This is a terminal state. + Delivery Timed Out: The command wasn't delivered to the managed node before the delivery timeout expired. Delivery timeouts don't count against the parent command's MaxErrors limit, but they do contribute to whether the parent command status is Success or Incomplete. This is a terminal state. + Execution Timed Out: The command started to run on the managed node, but the execution wasn't complete before the timeout expired. Execution timeouts count against the MaxErrors limit of the parent command. This is a terminal state. + Failed: The command wasn't run successfully on the managed node. For a plugin, this indicates that the result code wasn't zero. For a command invocation, this indicates that the result code for one or more plugins wasn't zero. Invocation failures count against the MaxErrors limit of the parent command. This is a terminal state. + Cancelled: The command was terminated before it was completed. This is a terminal state. + Undeliverable: The command can't be delivered to the managed node. The node might not exist or might not be responding. Undeliverable invocations don't count against the parent command's MaxErrors limit and don't contribute to whether the parent command status is Success or Incomplete. This is a terminal state. + Terminated: The parent command exceeded its MaxErrors limit and subsequent command invocations were canceled by the system. This is a terminal state. + #> + + # keep trying again + $keepLoopingStatuses = @('Pending', 'In Progress', 'Delayed') + # stop trying again + $stopLoopingStatuses = @('Success', 'Delivery Timed Out', 'Execution Timed Out', 'Failed', 'Cancelled', 'Undeliverable', 'Terminated') + # show an error + $failureStatuses = @('Delivery Timed Out', 'Execution Timed Out', 'Failed', 'Cancelled', 'Undeliverable', 'Terminated') + # show a success + $successStatuses = @('Success') + + while ($instanceIds.Count -gt 0) { + $finishedInstanceIds = @() + foreach ($instanceId in $instanceIds) { + try { + $instanceResultString = (aws ssm get-command-invocation --command-id $commandId --instance-id $instanceId) + + $instanceResult = ConvertFrom-Json $instanceResultString -Depth 100 + + if ($keepLoopingStatuses -contains $instanceResult.StatusDetails) { + Write-Host "$logLead : Will recheck status on [$instanceId] after sleep because it is still state [$($instanceResult.StatusDetails)]" + } + if ($stopLoopingStatuses -contains $instanceResult.StatusDetails) { + if ($successStatuses -contains $instanceResult.StatusDetails) { + Write-Host "$logLead : Instance [$instanceId] successfully finished the process" + } + if ($failureStatuses -contains $instanceResult.StatusDetails) { + Write-Warning "$logLead : Instance [$instanceId] finished the process with state [$(instanceResult.StatusDetails)]" + Write-Host "$logLead : For the following results, please see https://docs.aws.amazon.com/cli/latest/reference/ssm/get-command-invocation.html" + } + # log "normal" properties + foreach ($property in @('Comment', 'StandardOutputContent', 'StandardOutputUrl')) { + if (![string]::IsNullOrWhiteSpace($instanceResult.$property)) { + Write-Host "$logLead : $instanceId : $property : $($instanceResult.$property)" + } + } + # log "warning" properties + foreach ($property in @('StandardErrorUrl', 'StandardErrorContent')) { + if (![string]::IsNullOrWhiteSpace($instanceResult.$property)) { + Write-Warning "$logLead : $instanceId : $property : $($instanceResult.$property)" + } + } + + $finishedInstanceIds += $instanceId + } + } catch { + $finishedInstanceIds += $instanceId + } + } + + $instanceIds = $instanceIds | Where-Object { $finishedInstanceIds -notcontains $PSItem } + + if ($instanceIds.Count -gt 0) { + Start-Sleep -Seconds 5 + } + + Write-Host "$logLead : Still waiting to check the results on [$($instanceIds -join ',')]" + } +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Invoke-UpdateChrome.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Invoke-UpdateChrome.ps1 new file mode 100644 index 0000000..9d62ad7 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Invoke-UpdateChrome.ps1 @@ -0,0 +1,27 @@ +function Invoke-UpdateChrome { + <# +.SYNOPSIS + Upgrade Chrome on the given servers using the internally managed Choco package GoogleChrome +.PARAMETER ComputerName + List of servers to run the upgrade on +.PARAMETER WhatIf + Safety valve +#> + [CmdletBinding()] + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$ComputerName, + [Parameter(Mandatory = $false)] + [switch]$WhatIf + ) + $sbChrome = { + choco upgrade -y GoogleChrome + } + if ($WhatIf) { + Write-Host "Calling Invoke-Command -ComputerName $ComputerName -ScriptBlock $sbChrome" + } else { + Invoke-Command -ComputerName $ComputerName -ScriptBlock $sbChrome + } +} diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraRuntimes.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraRuntimes.ps1 new file mode 100644 index 0000000..4063793 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraRuntimes.ps1 @@ -0,0 +1,22 @@ +Function Remove-DotnetExtraRuntimes { + <# +.SYNOPSIS + Delete extra runtimes for dotnet. Ensure that we only keep the latest around. + This assumes that minors are as distinct as majors. + +.EXAMPLE + Remove-DotnetExtraRuntimes +#> + + [CmdletBinding()] + param() + + $logLead = Get-LogLeadName + + $versionsToDelete = Get-DotnetExtraRuntimes -ReturnExpiredRuntimes + + foreach ($deleteRuntime in $versionsToDelete) { + Write-Host "$logLead : Deleting [$deleteRuntime]" + Remove-Item -Recurse -Force -Path $deleteRuntime + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraSDKs.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraSDKs.ps1 new file mode 100644 index 0000000..e90da37 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Remove-DotnetExtraSDKs.ps1 @@ -0,0 +1,22 @@ +Function Remove-DotnetExtraSDKs { + <# +.SYNOPSIS + Delete extra runtimes for dotnet. Ensure that we only keep the latest around. + This assumes that minors are as distinct as majors. + +.EXAMPLE + Remove-DotnetExtraSDKs +#> + + [CmdletBinding()] + param() + + $logLead = Get-LogLeadName + + $sdksToDelete = Get-DotnetExtraSDKs -ReturnExpiredSdks + + foreach ($deleteSdk in $sdksToDelete) { + Write-Host "$logLead : Deleting [$deleteSdk]" + Remove-Item -Recurse -Force -Path $deleteSdk + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Start-TeamCityAgent.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Start-TeamCityAgent.ps1 new file mode 100644 index 0000000..1c90365 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Start-TeamCityAgent.ps1 @@ -0,0 +1,79 @@ +function Start-TeamCityAgent { + <# +.SYNOPSIS + Start the TeamCity Agent service(s) on a given host or on the host(s) in a given Agent Pool + +.PARAMETER ComputerName + FQDN hostname of the TeamCity agent host where you want to start the Agent service + +.PARAMETER AgentPool + TeamCity Agent Pool name of the TeamCity agents you want to start + +.LINK + Get-TeamCityAgentHostnames + +.LINK + Stop-TeamCityAgent + +.LINK + https://ci.corp.alkamitech.com/agents.html?tab=agentPools + +.LINK + https://ci.corp.alkamitech.com/app/rest/agentPools +#> + [CmdletBinding()] + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true, ParameterSetName = "ComputerName")] + $ComputerName, + [Parameter(Mandatory = $true, ParameterSetName = "AgentPool")] + $AgentPool + ) + + # Why would you name the Keys like that, Tom? + # + # Because that's what they're named in TeamCity. Ideally, we'd be able to pull from the API + # For a lot of things eventually and I don't want to make names up. + # If you don't know the name, look it up in TeamCity. That's the name. + # Also... + # There are other pools that we don't touch the hosts of. "All" isn't really "All" + $agentPoolToServiceNameFragments = @{ + "Sandbox" = @('buildSandbox', 'migrateSandbox') + "Dev/QA" = @('migrateQaDev', 'buildQaDev') + "Production/Staging" = @('buildagent', 'migrateagent') + "All" = @('buildSandbox', 'migrateSandbox', 'migrateQaDev', 'buildQaDev', 'buildagent', 'migrateagent') + } + # I hate everything about this except the last entry + # I really want to be getting these from the REST API. Or something. ANYthing else. + $agentPoolToAgentHostname = @{ + "Sandbox" = @("tea316229.fh.local", "tea37021.fh.local") + "Dev/QA" = @("tea316208.fh.local", "tea370104.fh.local") + "Production/Staging" = @("tea316155.fh.local", "tea31697.fh.local") + "All" = Get-TeamCityAgentHostnames + } + + + if ($PSCmdlet.ParameterSetName -eq "AgentPool") { + $ComputerName = $agentPoolToAgentHostname[$AgentPool] + } + + if ($PSCmdlet.ParameterSetName -eq "ComputerName") { + $AgentPool = "All" + } + + $agentServiceFragmentNames = $agentPoolToServiceNameFragments[$AgentPool] + + $sbStartAgent = { + param($sbServiceNameFragments) + $serviceNames = @() + foreach ($serviceName in $sbServiceNameFragments) { + $temp = (Get-ServiceInfoByCIMFragment $serviceName).name + if ($null -ne $temp) { + $serviceNames += $temp + } + } + Start-Service $serviceNames + } + Invoke-ParallelServers -Servers $ComputerName -Script $sbStartAgent -Arguments ($agentServiceFragmentNames) + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Stop-TeamCityAgent.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Stop-TeamCityAgent.ps1 new file mode 100644 index 0000000..0def6f3 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Stop-TeamCityAgent.ps1 @@ -0,0 +1,79 @@ +function Stop-TeamCityAgent { + <# +.SYNOPSIS + Stop the TeamCity Agent service(s) on a given host or on the host(s) in a given Agent Pool + +.PARAMETER ComputerName + FQDN hostname of the TeamCity agent host where you want to stop the Agent service + +.PARAMETER AgentPool + TeamCity Agent Pool name of the TeamCity agents you want to stop - this must be the EXACT NAME FROM TEAMCITY + +.LINK + Get-TeamCityAgentHostnames + +.LINK + Start-TeamCityAgent + +.LINK + https://ci.corp.alkamitech.com/agents.html?tab=agentPools + +.LINK + https://ci.corp.alkamitech.com/app/rest/agentPools +#> + [CmdletBinding()] + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true, ParameterSetName = "ComputerName")] + $ComputerName, + [Parameter(Mandatory = $true, ParameterSetName = "AgentPool")] + $AgentPool + ) + + # Why would you name the Keys like that, Tom? + # + # Because that's what they're named in TeamCity. Ideally, we'd be able to pull from the API + # For a lot of things eventually and I don't want to make names up. + # If you don't know the name, look it up in TeamCity. That's the name. + # Also... + # There are other pools that we don't touch the hosts of. "All" isn't really "All" + $agentPoolToServiceNameFragments = @{ + "Sandbox" = @('buildSandbox', 'migrateSandbox') + "Dev/QA" = @('migrateQaDev', 'buildQaDev') + "Production/Staging" = @('buildagent', 'migrateagent') + "All" = @('buildSandbox', 'migrateSandbox', 'migrateQaDev', 'buildQaDev', 'buildagent', 'migrateagent') + } + # I hate everything about this except the last entry + # I really want to be getting these from the REST API. Or something. ANYthing else. + $agentPoolToAgentHostname = @{ + "Sandbox" = @("tea316229.fh.local", "tea37021.fh.local") + "Dev/QA" = @("tea316208.fh.local", "tea370104.fh.local") + "Production/Staging" = @("tea316155.fh.local", "tea31697.fh.local") + "All" = Get-TeamCityAgentHostnames + } + + if ($PSCmdlet.ParameterSetName -eq "AgentPool") { + $ComputerName = $agentPoolToAgentHostname[$AgentPool] + } + + if ($PSCmdlet.ParameterSetName -eq "ComputerName") { + $AgentPool = "All" + } + + $agentServiceFragmentNames = $agentPoolToServiceNameFragments[$AgentPool] + + $sbStopAgent = { + param($sbServiceNameFragments) + $serviceNames = @() + foreach ($serviceName in $sbServiceNameFragments) { + $temp = (Get-ServiceInfoByCIMFragment $serviceName).name + if ($null -ne $temp) { + $serviceNames += $temp + } + } + Stop-Service $serviceNames + } + + Invoke-ParallelServers -Servers $ComputerName -Script $sbStopAgent -Arguments ($agentServiceFragmentNames) + +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/Public/Test-TeamCityComputerIsAvailable.ps1 b/Modules/Alkami.DevOps.TeamCity/Public/Test-TeamCityComputerIsAvailable.ps1 new file mode 100644 index 0000000..d185a58 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/Public/Test-TeamCityComputerIsAvailable.ps1 @@ -0,0 +1,35 @@ +function Test-TeamCityComputerIsAvailable { + <# + .SYNOPSIS + Test that given computer(s) are on and able to be connected to + + .PARAMETER ComputerName + [string] Computer(s) to connect to. Assumes .fh.local domain + + .PARAMETER Type + Optional Parameter. Type of TeamCity host(s) to return: All, Server(s), or Agent(s) + + #> + [CmdletBinding()] + [OutputType([Object])] + param ( + [Parameter(Mandatory = $false)] + [string[]]$ComputerName, + [ValidateSet("All", "Server", "Agent")] + [Parameter(Mandatory = $false)] + [string]$Type + ) + $logLead = Get-LogLeadName + $resultObject = @{} + + if (!(Test-StringIsNullOrWhitespace -Value $Type)) { + $ComputerName = Get-TeamCityHostnames -Type $Type + } + + foreach ($computer in $ComputerName) { + $result = Test-ComputerIsAvailable -ComputerName $computer + Write-Host "$logLead : $computer : $result" + $resultObject += @{$computer = "$result"} + } + Return $resultObject +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.TeamCity/thunder-collection_TeamCity.json b/Modules/Alkami.DevOps.TeamCity/thunder-collection_TeamCity.json new file mode 100644 index 0000000..958b9a1 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/thunder-collection_TeamCity.json @@ -0,0 +1 @@ +{"client":"Thunder Client","collectionName":"TeamCity","dateExported":"2022-05-13T20:18:58.311Z","version":"1.1","folders":[{"_id":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"Modules","containerId":"","created":"2022-04-21T16:19:50.730Z","sortNum":10000},{"_id":"69c6e672-6574-4c03-a8e4-a9e84e139fca","name":"PinnedBuilds","containerId":"","created":"2022-05-13T20:15:06.282Z","sortNum":20000}],"requests":[{"_id":"52e170cd-91fb-43aa-89fa-eee41988333b","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"PushBranchModules","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2158091","method":"GET","sortNum":10000,"created":"2022-04-21T16:06:23.478Z","modified":"2022-04-25T19:32:55.423Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"231dbe19-00a8-4662-944d-bc79982d8e67","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"PushBranchModules Copy","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2158091","method":"GET","sortNum":12500,"created":"2022-04-27T14:02:02.004Z","modified":"2022-04-27T14:02:25.900Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"basic","basic":{"username":"sreapiuser","password":"Ffa4NPQdA7Cw"}},"tests":[]},{"_id":"adab8186-6ad3-4573-a39e-0c8c5907f467","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"PushBranchModulesSnapshotDeps","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2158091?fields=snapshot-dependencies(count,build)","method":"GET","sortNum":15000,"created":"2022-04-22T14:59:40.041Z","modified":"2022-04-25T19:33:17.255Z","headers":[{"name":"Accept","value":"application/json"}],"params":[{"name":"fields","value":"snapshot-dependencies(count,build)","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"57b18827-fd6b-423f-8b3f-972c6ed8dbfb","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCServer","url":"https://ci.corp.alkamitech.com/app/rest","method":"GET","sortNum":20000,"created":"2021-05-18T15:08:20.177Z","modified":"2021-05-18T15:08:43.302Z","headers":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"b01917ba-063b-4337-b6e5-78e80a273a69","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"BranchModuleDependency","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2158073","method":"GET","sortNum":20000,"created":"2022-04-21T16:08:35.911Z","modified":"2022-04-25T19:34:25.661Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"04b81ce0-4f3b-465b-bd9a-af9858f1502e","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCServer Copy","url":"https://ci.corp.alkamitech.com/app/rest/server/$help","method":"GET","sortNum":20625,"created":"2022-04-21T16:29:24.379Z","modified":"2022-04-21T16:31:30.428Z","headers":[],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"5bd0f061-43eb-475b-94a0-ff23da62fc4a","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCAudit","url":"https://ci.corp.alkamitech.com/app/rest/audit?locator=$help","method":"GET","sortNum":21250,"created":"2021-06-03T17:40:16.648Z","modified":"2021-11-10T15:29:46.192Z","headers":[],"params":[{"name":"locator","value":"$help","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"3627b058-cb00-472b-a0bf-96d237172e10","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCBuild","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:1780304","method":"GET","sortNum":22500,"created":"2021-05-20T15:46:18.817Z","modified":"2021-11-05T17:46:52.576Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"934a768d-e4fc-481a-b6a3-5f82c4439604","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCBuildWithTriggeredInfo","url":"https://ci.corp.alkamitech.com/app/rest/builds/1780304?fields=statusText,branchName,triggered(user,build,buildType),status,testOccurrences,buildType(id,name),running-info,properties(property)","method":"GET","sortNum":23750,"created":"2021-05-20T18:17:30.882Z","modified":"2022-04-13T16:02:36.453Z","headers":[{"name":"Accept","value":"application/json"}],"params":[{"name":"fields","value":"statusText,branchName,triggered(user,build,buildType),status,testOccurrences,buildType(id,name),running-info,properties(property)","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"efe8ead8-bf06-45ed-a293-ccc0222799ae","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"TCUser","url":"https://ci.corp.alkamitech.com/app/rest/users/trowton","method":"GET","sortNum":25000,"created":"2021-05-19T19:17:44.482Z","modified":"2021-11-10T14:57:51.575Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"4bfd72fa-9f20-4f52-a3fd-9bc26ac2828a","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"BranchModuleDependency Copy","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2158073?fields=number,branchName,triggered(user(username)),properties(property)","method":"GET","sortNum":25000,"created":"2022-04-25T18:49:10.088Z","modified":"2022-04-25T22:02:31.270Z","headers":[{"name":"Accept","value":"application/json"}],"params":[{"name":"fields","value":"number,branchName,triggered(user(username)),properties(property)","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"a7601a7d-bb1e-46e3-bc97-3d1dfec339bc","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"BuildType","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:MaintenancePages_Patelco_API_PatelcoMaintenance","method":"GET","sortNum":30000,"created":"2021-05-18T15:09:45.772Z","modified":"2022-02-19T19:00:14.728Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"115d5db4-b0e0-4332-b623-fd5027c0adb4","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"BranchModuleDependencyArtifacts","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2139979/artifacts/children?fields=file(name)","method":"GET","sortNum":30000,"created":"2022-04-21T16:11:56.603Z","modified":"2022-04-21T16:22:12.464Z","headers":[{"name":"Accept","value":"application/json"}],"params":[{"name":"fields","value":"file(name)","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"2e0f3337-072c-4aac-9245-ab2c03ed8488","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"BuildType Copy","url":"https://ci.corp.alkamitech.com/app/rest/projects/id:MaintenancePages","method":"GET","sortNum":32500,"created":"2022-04-05T20:23:18.782Z","modified":"2022-04-05T20:35:41.171Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"5b0bc07b-6eaf-41c0-9a49-60f71710d3ea","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"BuildType Triggers","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:MaintenancePages_Patelco_API_PatelcoMaintenance/triggers","method":"GET","sortNum":35000,"created":"2022-03-09T15:47:47.311Z","modified":"2022-03-09T15:49:32.883Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"43ea9496-bc8a-46de-8322-273a3b2c6bba","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"Snapshot Deps","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:DeployV3_DeploymentDependencies_PostDeployStartToChat/snapshot-dependencies","method":"GET","sortNum":40000,"created":"2021-05-18T15:15:43.690Z","modified":"2021-05-24T18:03:07.306Z","headers":[{"name":"Accept","value":"application/json"}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"86f1b3eb-0648-475e-8bb9-1008b6f189b0","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"BranchModulesBuildModulesZipFile","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2139968","method":"GET","sortNum":40000,"created":"2022-04-21T16:15:19.382Z","modified":"2022-04-21T16:20:33.942Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"ba7f6b68-82d3-4f1c-8029-37ed4a4f0178","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"Snapshot Deps Update NR","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:DeployV3_DeploymentDependencies_PostNewRelicDeploymentInformation/snapshot-dependencies","method":"GET","sortNum":50000,"created":"2021-05-25T13:32:56.248Z","modified":"2021-05-25T13:33:18.396Z","headers":[{"name":"Accept","value":"application/json"}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"1a429957-3016-4e5e-ab02-3d4fa9fffd1f","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"c1df835a-dd84-43c1-a128-a6ba6b2c21d6","name":"BranchModulesBuildModulesZipFileArtifact","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:2139968/artifacts/children","method":"GET","sortNum":50000,"created":"2022-04-21T16:18:17.541Z","modified":"2022-04-21T16:20:38.533Z","headers":[{"name":"Accept","value":"application/json"}],"params":[],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"ebcb0d0a-c241-406a-81af-7b7166a73489","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"Snapshot Deps Deploy Log4Net Defaults","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:DeployV3_DeploymentDependencies_CallBounceServices/snapshot-dependencies","method":"GET","sortNum":60000,"created":"2021-05-25T13:41:32.888Z","modified":"2021-05-25T13:57:41.879Z","headers":[{"name":"Accept","value":"application/json"}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"7d12d9b0-ac3e-4c20-a3b7-7d77cbef7871","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"","name":"Snapshot Deps CallBounceServices","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/id:DeployV3_DeploymentDependencies_DeployLog4netDefaults/snapshot-dependencies","method":"GET","sortNum":70000,"created":"2021-05-25T13:57:05.278Z","modified":"2021-05-25T13:57:05.278Z","headers":[{"name":"Accept","value":"application/json"}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"3a5d6b22-11a4-45b3-883a-8b17f82c8f05","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"69c6e672-6574-4c03-a8e4-a9e84e139fca","name":"DeployV3ProdPinned","url":"https://ci.corp.alkamitech.com/app/rest/buildTypes/project%3ADeployV3_Production/builds?onlyPinned=true","method":"GET","sortNum":80000,"created":"2022-05-13T20:15:30.008Z","modified":"2022-05-13T20:16:34.699Z","headers":[],"params":[{"name":"onlyPinned","value":"true","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]},{"_id":"d7585c74-2aad-47ae-8792-ace510a39f58","colId":"825fbd0a-c490-4e6f-a8b1-728bb82ee2f4","containerId":"69c6e672-6574-4c03-a8e4-a9e84e139fca","name":"BuildWithPinnedInfo","url":"https://ci.corp.alkamitech.com/app/rest/builds/id:1077853?fields=href,webUrl,pinInfo,statusText","method":"GET","sortNum":90000,"created":"2022-05-13T20:17:11.108Z","modified":"2022-05-13T20:17:45.676Z","headers":[],"params":[{"name":"fields","value":"href,webUrl,pinInfo,statusText","isPath":false}],"auth":{"type":"bearer","bearer":"{{TCTOKEN}}"},"tests":[]}]} diff --git a/Modules/Alkami.DevOps.TeamCity/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.TeamCity/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..74befcf --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/tools/chocolateyInstall.ps1 @@ -0,0 +1,38 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + ## Ensure the module was able to load + Import-Module $id -Global; +} diff --git a/Modules/Alkami.DevOps.TeamCity/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.TeamCity/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..29b2f77 --- /dev/null +++ b/Modules/Alkami.DevOps.TeamCity/tools/chocolateyUninstall.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.nuspec b/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.nuspec new file mode 100644 index 0000000..791da77 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.nuspec @@ -0,0 +1,27 @@ + + + + Alkami.DevOps.Validations + $version$ + Alkami Platform Modules - DevOps - Validations + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Validations + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami DevOps Validations module for use with PowerShell. + + PowerShell + Copyright (c) 2020 Alkami Technologies + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.psd1 b/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.psd1 new file mode 100644 index 0000000..1217bfb --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Alkami.DevOps.Validations.psd1 @@ -0,0 +1,19 @@ +@{ + RootModule = 'Alkami.DevOps.Validations.psm1' + ModuleVersion = '1.4.5' + GUID = 'd3b31a83-eb01-4d3a-a0af-a37b08b4bf5b' + Author = 'SRE,jcoburn' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2020 Alkami Technologies, Inc.. All rights reserved.' + Description = 'Functions used to validate ORB deployment success using web requests.' + FileList = @('Resources\ObjectAsTableTemplate.html') + FunctionsToExport = 'Get-CoreUrlsToTest','Get-LoginsToTest','Get-WebTestLogPath','Invoke-Endpoint','Invoke-WebTests','Reset-WebTestLogFolder','Set-RedisToken','Start-WebTests','Test-HaveAccountCount','Test-HaveContentThatMatches','Test-HaveResponseHeader','Test-HaveStatusCode','Test-Passthrough','Test-Should' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'validations', 'smoke', 'smoketest') + ProjectUri = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Validations+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Validations+Module' +} diff --git a/Modules/Alkami.DevOps.Validations/AlkamiManifest.xml b/Modules/Alkami.DevOps.Validations/AlkamiManifest.xml new file mode 100644 index 0000000..abf7751 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.DevOps.Validations + SREModule + + + Production + + diff --git a/Modules/Alkami.DevOps.Validations/Public/Get-CoreUrlsToTest.ps1 b/Modules/Alkami.DevOps.Validations/Public/Get-CoreUrlsToTest.ps1 new file mode 100644 index 0000000..bd718da --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Get-CoreUrlsToTest.ps1 @@ -0,0 +1,49 @@ +function Get-CoreUrlsToTest { + <# + .SYNOPSIS + Gets the URLs of the websites to test + + .DESCRIPTION + Currently pulls from S3 bucket to get a .json file of URLs. + + .PARAMETER AwsProfile + Aws profile for connecting to S3 + + .NOTES + Returns a JSON string + #> + [cmdletbinding()] + [OutputType([System.String])] + Param ( + [Parameter(Mandatory = $true)] + [string]$AwsProfile + ) + + $logLead = (Get-LogLeadName) + $environmentType = $AwsProfile.Replace("temp-","").ToLower() + $bucketName = "alkami-devops-validations-$environmentType" + $key = "coreurls.json" + $fileName = [System.IO.Path]::GetRandomFileName() + + $S3Script = { + param($sbBucketName, $sbKey, $sbFileName, $sbAwsProfile) + (Read-S3Object -BucketName $sbBucketName -Key $sbKey -File $sbFileName -ProfileName $sbAwsProfile) | Out-Null + } + + try{ + Write-Host "$logLead Getting file from S3." + (Invoke-CommandWithRetry -Arguments ($bucketName, $key, $fileName, $AwsProfile) -MaxRetries 3 -Exponential -ScriptBlock $S3Script) + Write-Host "$logLead Getting content from file." + $contentJson = Get-Content -Path ".\$fileName" -Raw -ErrorAction SilentlyContinue + if($null -eq $contentJson) { + throw "Could not get Json content from file!" + } + } finally { + if(Test-Path ".\$fileName" -PathType Leaf) { + Write-Host "$logLead Removing file [$fileName]" + (Remove-FileSystemItem -Path ".\$fileName") | Out-Null + } + } + + return ($contentJson | Out-String) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Get-LoginsToTest.ps1 b/Modules/Alkami.DevOps.Validations/Public/Get-LoginsToTest.ps1 new file mode 100644 index 0000000..cfb4746 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Get-LoginsToTest.ps1 @@ -0,0 +1,49 @@ +function Get-LoginsToTest { + <# + .SYNOPSIS + Gets logins to perform validation web tests against. + + .DESCRIPTION + Currently pulls from S3 bucket to get a .json file of logins. + + .PARAMETER AwsProfile + Aws profile for connecting to S3 + + .NOTES + Returns a JSON string + #> + [cmdletbinding()] + [OutputType([System.String])] + Param ( + [Parameter(Mandatory = $true)] + [string]$AwsProfile + ) + + $logLead = (Get-LogLeadName) + $environmentType = $AwsProfile.Replace("temp-","").ToLower() + $bucketName = "alkami-devops-validations-$environmentType" + $key = "logins.json" + $fileName = [System.IO.Path]::GetRandomFileName() + + $S3Script = { + param($sbBucketName, $sbKey, $sbFileName, $sbAwsProfile) + (Read-S3Object -BucketName $sbBucketName -Key $sbKey -File $sbFileName -ProfileName $sbAwsProfile) | Out-Null + } + + try{ + Write-Host "$logLead Getting file from S3." + (Invoke-CommandWithRetry -Arguments ($bucketName, $key, $fileName, $AwsProfile) -MaxRetries 3 -Exponential -ScriptBlock $S3Script) + Write-Host "$logLead Getting content from file." + $contentJson = Get-Content -Path ".\$fileName" -Raw -ErrorAction SilentlyContinue + if($null -eq $contentJson) { + throw "Could not get Json content from file!" + } + } finally { + if(Test-Path ".\$fileName" -PathType Leaf) { + Write-Host "$logLead Removing file [$fileName]" + (Remove-FileSystemItem -Path ".\$fileName") | Out-Null + } + } + + return ($contentJson | Out-String) +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Get-WebTestLogPath.ps1 b/Modules/Alkami.DevOps.Validations/Public/Get-WebTestLogPath.ps1 new file mode 100644 index 0000000..efe9df4 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Get-WebTestLogPath.ps1 @@ -0,0 +1,63 @@ +function Get-WebTestLogPath { +<# +.SYNOPSIS + Get the web test log path for logging data mid-run so we can track what happens on disk during runtime testing. + This function is intended to be used in ancillary test logging. + +.DESCRIPTION + The impetus for this change is that some things were breaking unexpectedly during testing and crashing the entire host. + We needed a way to track where it was crashing the host mid-process. Hence this function and .. + Write-* "message" + becomes + "message" | Tee-Object -Append -Path $thisPath | Write-* + +.PARAMETER BankUrl + [Required] [string] The bank url to log for + +.PARAMETER Widget + [Optional] [string] The widget to log for. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Alias("LogName")] + [string]$BankUrl, + + [Parameter(Mandatory = $false)] + [string]$Widget + ) + + $logFilePath = (Join-Path (Get-OrbLogsPath) WebTests) + if (!(Test-Path $logFilePath)) { + New-Item -ItemType Directory -Path $logFilePath -Force + } + + $logUrlFilename = $BankUrl + if ($BankUrl.ToLower().StartsWith("http")) { + $uri = [System.Uri]::new($BankUrl) + $logUrlFilename = $uri.DnsSafeHost + + # If the widget wasn't passed in + if ([string]::IsNullOrWhiteSpace($Widget)) { + # and the url had a path + $potentialWidget = $uri.LocalPath.Split('/')[1] + if (![string]::IsNullOrWhiteSpace($potentialWidget)) { + # grab the first part of that path as the widget name + $Widget = $potentialWidget + } + } + } + + $baseLogUrlFilename = $logUrlFilename + if (![string]::IsNullOrWhiteSpace($Widget)) { + $logUrlFilename = "$logUrlFilename.$Widget" + } + + $finalLogFilePath = (Join-Path $logFilePath "$logUrlFilename.log") + + if (!(Test-Path $finalLogFilePath)) { + $finalLogFilePath = (Join-Path $logFilePath "$baseLogUrlFilename.log") + } + + return $finalLogFilePath +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Invoke-Endpoint.ps1 b/Modules/Alkami.DevOps.Validations/Public/Invoke-Endpoint.ps1 new file mode 100644 index 0000000..ebe635b --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Invoke-Endpoint.ps1 @@ -0,0 +1,190 @@ +function Invoke-Endpoint { + <# + .SYNOPSIS + Invokes a website and returns the response of the URL + + .DESCRIPTION + Wraps Invoke-WebRequest with retry logic + + .PARAMETER Uri + URI of the website to invoke + + .PARAMETER Method + HTTP method, Get, Post, etc. + + .PARAMETER Baseuri + Optional parameter taking the base Uri and concatinating with Uri. "$Baseuri$Uri". + Helper to avoid having to concatinate before invoking site. + + .PARAMETER retryStatusCodes + [System.Net.HttpStatusCode][] objects that are eligible for retry logic. + [System.Net.HttpStatusCode]::ServiceUnavailable is default. + + .PARAMETER RetryDelay + Seconds to wait between retries. Defaults to 5. Is exponential backoff. + + .PARAMETER RetryCount + Total number of retry attempts before failing. + + .PARAMETER Body + Used for POST Method + + .PARAMETER Headers + String of Http headers to pass to the request. + + .PARAMETER WebSession + Shared session to use for subsequent requests + + .PARAMETER UseNewSession + Bool to determine if a new blank session will be used and returned instead of the passed-in WebSession. + + .PARAMETER TimeoutSeconds + Int in seconds for the web timeout. + + .EXAMPLE + $r = Invoke-Endpoint -Uri http://google.com + #> + [cmdletbinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)] + $Uri, + [Parameter(Mandatory = $false)] + $Method = 'GET', + [Parameter(Mandatory = $false)] + $Baseuri = '', + [Parameter(Mandatory=$false)] + [System.Net.HttpStatusCode[]] $retryStatusCodes, + [Parameter(Mandatory=$false)] + $RetryDelay = 5, + [Parameter(Mandatory=$false)] + $RetryCount = 3, + [Parameter(Mandatory=$false)] + $Body = $null, + [Parameter(Mandatory=$false)] + $Headers = $null, + [Parameter(Mandatory=$false)] + $WebSession = $null, + [Parameter(Mandatory=$false)] + [bool]$UseNewSession = $false, + [Parameter(Mandatory=$false)] + [int]$TimeoutSeconds = 120 + ) + begin { + $auldProgressPreference = $ProgressPreference + $ProgressPreference = 'silentlycontinue' + } + process { + $logLead = (Get-LogLeadName) + $logFilePath = (Get-WebTestLogPath -BankUrl "$Baseuri$Uri") + + $retryCounter = 0 + + $totalRuntime = 0 + $errorsArray = @() + $runtimeArray = @() + $result = $null + + if($null -eq $retryStatusCodes) { + $retryStatusCodes = @( + [System.Net.HttpStatusCode]::BadGateway, + [System.Net.HttpStatusCode]::BadRequest, + [System.Net.HttpStatusCode]::GatewayTimeout, + [System.Net.HttpStatusCode]::InternalServerError, + [System.Net.HttpStatusCode]::ServiceUnavailable + ) + } + + do { + $StopWatch = [System.Diagnostics.StopWatch]::StartNew() + try { + "$logLead : Performing web request to $($baseuri)$($uri)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + if($UseNewSession) { + $result = Invoke-WebRequest "$Baseuri$Uri" -Method $Method -Body $Body -Headers $Headers -SessionVariable NewWebSession -UseBasicParsing -TimeoutSec $TimeoutSeconds -DisableKeepAlive -UserAgent "SRE Web Test" + $WebSession = $NewWebSession + } else { + $result = Invoke-WebRequest "$Baseuri$Uri" -Method $Method -Body $Body -Headers $Headers -WebSession $WebSession -UseBasicParsing -TimeoutSec $TimeoutSeconds -DisableKeepAlive -UserAgent "SRE Web Test" + } + # On success, stop executing + break + } + catch [System.Net.WebException] { + "$logLead : Exception: $_" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $errorsArray += $_.Exception.Message + if ($null -eq $_.Exception.Response -and $_.Exception.Message -eq "The operation has timed out.") { + "$logLead : No response object found. Request timed out." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $retryCounter += 1 + $hasRetriedEnough = $retryCounter -gt ($retryCount) + if ($hasRetriedEnough) { + "$logLead : Returning response object" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + break + } else { + $sleepTime = $retryDelay*$retryCounter + "$logLead : Sleeping for $sleepTime seconds" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + Start-Sleep -Seconds $sleepTime + $resp = $null + } + } else { + $resp = $_.Exception.Response + "$logLead : Response code: $($resp.StatusCode)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $retryCounter += 1 + $isARetryCode = $retryStatusCodes -contains $resp.StatusCode + "$logLead : Is retry code: $isARetryCode" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $hasRetriedEnough = $retryCounter -gt ($retryCount) + "$logLead : Has retried enough: $hasRetriedEnough" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + if ($hasRetriedEnough -or(-not($isARetryCode))) { + "$logLead : Returning response object" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + break + } else { + $sleepTime = $retryDelay*$retryCounter + "$logLead : Sleeping for $sleepTime seconds" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + Start-Sleep -Seconds $sleepTime + } + } + } + catch { + $errorsArray += $_.Exception.Message + "$logLead : Got a generic exception. Quitting immediately" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + break + } finally { + $StopWatch.Stop() + $totalRuntime += $StopWatch.ElapsedMilliseconds + $runtimeArray += $StopWatch.Elapsed.ToString("ss\.fffffff") + + $isSuccess = $true + + if($result.StatusCode -ne 200){ + if($null -eq $resp){ + # If we got a timeout, send a null result + Write-Verbose "$logLead : Request timed out." + $result = $null + } else { + # If we got a failure response, map the most recent exception's data onto the result and give that back. + Write-Verbose "$logLead : Got a $($resp.StatusCode)" + $result = $resp + } + + $isSuccess = $false + } + + $responseObject = @{ + ErrorResults = $errorsArray; + LastError = $errorsArray[-1]; + HasErrors = $errorsArray.Count -gt 0; + Result = $result; + Success = $isSuccess; + StatusCode = $result.StatusCode; + WebSession = $WebSession; + Runtimes = $runtimeArray; + LastRuntime = $runtimeArray[-1]; + TotalRuntime = [System.TimeSpan]::FromMilliseconds($totalRuntime).ToString() + } + $result = $null + } + } while ($true) + + return $responseObject + } + end { + $ProgressPreference = $auldProgressPreference + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Invoke-WebTests.ps1 b/Modules/Alkami.DevOps.Validations/Public/Invoke-WebTests.ps1 new file mode 100644 index 0000000..773b8ef --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Invoke-WebTests.ps1 @@ -0,0 +1,172 @@ +function Invoke-WebTests { + <# + .SYNOPSIS + Helper function that wraps Start-WebTests and sets up the variables needed for Start-WebTests. + + .DESCRIPTION + This is intended to be used from a TeamCity agent as it uses Invoke-Command -Script {Start-WebTests} + which are to be run on each web-tier server. + + .PARAMETER Servers + String CSV of servers to run the Start-WebTests on. + + .PARAMETER AwsProfile + Aws Profile to use for S3 bucket. + + .PARAMETER EnvironmentType + Which environment (dev, prod) to run the tests on. Uses different URLs based on environmentType. + + .PARAMETER SitesToSkip + A list of sites to skip web tests on. Http/https will be trimmed + + .PARAMETER SkipWidgetWarmup + Bool to skip warming up the widgets with a throw-away http request to all configured widgets on a site. + Defaults to true (skip widget warmup). + + .NOTES + Keeps track of all the tests and will return bool if they all pass or any fail. + return $PassedAllTests + #> + [cmdletbinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $true)] + [string[]]$Servers, + [Parameter(Mandatory = $false)] + [string]$AwsProfile, + [Parameter(Mandatory = $false)] + [string[]]$SitesToSkip, + [Parameter(Mandatory = $false)] + [bool]$SkipWidgetWarmUp = $true, + [Parameter(Mandatory = $false)] + [int]$MaximumSitesToTest = 20, + [Parameter(Mandatory = $false)] + [int]$SiteThreads = 4, + [Parameter(Mandatory = $false)] + [int]$WidgetThreads = 8, + [Parameter(Mandatory = $false)] + [string[]]$ServerTargetOverride + ) + + if([string]::IsNullOrEmpty($AwsProfile)) { + $AwsProfile = "prod" + } + + $logLead = (Get-LogLeadName) + $passedAllTests = $true + + $sitesToSkipArray = @() + # We do a type check here to determine if this was called directly from TC or not. If yes, clean it up. + if($null -ne $SitesToSkip -and $SitesToSkip -match ",|\s+") { + Write-Host "Got called from TC directly. Doing the splits for sites to skip." + # Split out individual sites, divided by commas or spaces. Ignore blanks. + $sitesToSkipArray = ($SitesToSkip -split ',|\s+') | Where-Object {("" -ne $_)} + } else { + $sitesToSkipArray = $SitesToSkip + } + + if (![String]::IsNullOrEmpty($ServerTargetOverride)) { + # Same type check here. Split override servers if there's more than one. + # Also split servers, because we're coming from TC, so it must be a string as well. + if(($Servers -match ",") -or ($ServerTargetOverride -match ",|\s+")) { + Write-Host "Got called from TC directly. Doing the splits for override servers." + [array]$overrideArray = ($ServerTargetOverride -split ',|\s+') | Where-Object {("" -ne $_)} + [array]$serverArray = $Servers.split(',') + + Write-Host "$logLead : ServerArray $serverArray" + Write-Host "$logLead : OverrideArray $overrideArray" + } else { + Write-Host "Got Arrays, passing through" + [array]$overrideArray = $ServerTargetOverride + [array]$serverArray = $Servers + } + + $unexpectedServers = $overrideArray.Where{ $_ -notin $serverArray } + + if (($null -ne $unexpectedServers) -and ($unexpectedServers.Count -gt 0)) { + throw "$logLead : Received unexpected servers/values in the Server Target Override List. Re-examine your parameter list. Unexpected: $unexpectedServers" + } else { + $finalServers = $overrideArray + } + } else { + $finalServers = $Servers + } + + # Get the logins + try{ + $loginsJson = Get-LoginsToTest -AwsProfile $AwsProfile + } catch { + Write-Warning "$logLead : Exception getting logins to test!" + $ex = $_ + Resolve-Error -ErrorRecord $ex + Write-Host "$logLead : Setting test results to failed, returning False." + return $false + } + + # Get the Urls + try { + $coreUrlsJson = Get-CoreUrlsToTest -AwsProfile $AwsProfile + } catch { + Write-Warning "$logLead : Exception getting core URLs to test!" + $ex = $_ + Resolve-Error -ErrorRecord $ex + Write-Host "$logLead : Setting test results to failed, returning False." + return $false + } + + # Kick off the webtests on the servers + try { + $serverScriptArguments = @{ + LoginsJson = $loginsJson + CoreUrlsJson = $coreUrlsJson + SitesToSkip = $sitesToSkipArray + MaximumSitesToTest = $MaximumSitesToTest + SkipWidgetWarmUp = $SkipWidgetWarmUp + SiteThreads = $SiteThreads + WidgetThreads = $WidgetThreads + } + + $results = Invoke-ParallelServers -Servers $finalServers -ReturnObjects -Arguments $serverScriptArguments -Script { + param($inputArguments) + + $server = (Get-FullyQualifiedServerName) + + $sbLoginsJson = $inputArguments.LoginsJson + $sbCoreUrlsJson = $inputArguments.CoreUrlsJson + $sbSitesToSkip = $inputArguments.SitesToSkip + $sbMaximumSitesToTest = $inputArguments.MaximumSitesToTest + $sbSkipWidgetWarmUp = $inputArguments.SkipWidgetWarmUp + $sbSiteThreads = $inputArguments.SiteThreads + $sbWidgetThreads = $inputArguments.WidgetThreads + + Write-Host "##teamcity[blockOpened name='$server']" + + $result = (Start-WebTests -LoginsJson $sbLoginsJson ` + -CoreUrlsJson $sbCoreUrlsJson ` + -SitesToSkip $sbSitesToSkip ` + -MaximumSitesToTest $sbMaximumSitesToTest ` + -SkipWidgetWarmUp $sbSkipWidgetWarmup ` + -SiteThreads $sbSiteThreads ` + -WidgetThreads $sbWidgetThreads) + + Write-Host "##teamcity[blockClosed name='$server']" + + return $result + } + + foreach($result in $results) { + if(!$result) { + $passedAllTests = $false + } + } + } catch { + Write-Warning "$logLead : Error during web tests!" + $ex = $_ + Resolve-Error -ErrorRecord $ex + Write-Host "$logLead : Setting test results to failed, returning False." + $passedAllTests = $false + } + + Write-Host "$logLead : Has passed all tests? : $passedAllTests" + return $passedAllTests +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Reset-WebTestLogFolder.ps1 b/Modules/Alkami.DevOps.Validations/Public/Reset-WebTestLogFolder.ps1 new file mode 100644 index 0000000..f4dadd4 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Reset-WebTestLogFolder.ps1 @@ -0,0 +1,42 @@ +function Reset-WebTestLogFolder { +<# +.SYNOPSIS + Reset the folder for web tests so this run is pristine + +.DESCRIPTION + The impetus for this change is that some things were breaking unexpectedly during testing and crashing the entire host. + We needed a way to track where it was crashing the host mid-process. Hence this function and .. + Write-* "message" + becomes + "message" | Tee-Object -Append -Path $thisPath | Write-* + +.PARAMETER BaseLogFileName + [Required] [string] The bank url to log for +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [Alias("LogFileName")] + [string]$BaseLogFileName + ) + + $logFilePath = (Join-Path (Get-OrbLogsPath) WebTests) + if (!(Test-Path $logFilePath)) { + (New-Item -ItemType Directory -Path $logFilePath -Force) | Out-Null + } + + $archiveFolderPath = (Join-Path $logFilePath Archive) + if (!(Test-Path $archiveFolderPath)) { + (New-Item -ItemType Directory -Path $archiveFolderPath -Force) | Out-Null + } + + $baseLogFilePath = (Join-Path $logFilePath "$BaseLogFileName.log") + + if (Test-Path $baseLogFilePath) { + $timestamp = (Get-Item $baseLogFilePath).CreationTime.ToString("yyyyMMddhhmm") + $zipFilePath = (Join-Path $archiveFolderPath "$BaseLogFileName.$timestamp.zip") + $files = (Get-ChildItem -File -Path $logFilePath).FullName + (Compress-Archive -DestinationPath $zipFilePath -Path $files -Force) | Out-Null + (Remove-Item -Path $files -Force) | Out-Null + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Set-RedisToken.ps1 b/Modules/Alkami.DevOps.Validations/Public/Set-RedisToken.ps1 new file mode 100644 index 0000000..a9f34b4 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Set-RedisToken.ps1 @@ -0,0 +1,68 @@ +function Set-RedisToken { + <# + .SYNOPSIS + Method to set a Redis token to simulate a login event from Synthetic user. + + .PARAMETER BankUrl + Full URL of the bank we're going to auth to. + + .PARAMETER UserName + Username of the Synthetic + + .PARAMETER ComparisonOrbVersion + Supply a string of the orb version you want to compare against for determining redis key string formatting + + .EXAMPLE + $nonce = Set-RedisToken -BankUrl https://cu1-red9.dev.alkamitech.com -UserName -mike.brady + + .NOTES + Returns a guid representing the nonce that was inserted + #> + [cmdletbinding()] + param ( + [Parameter(Mandatory = $true)] + $BankUrl, + [Parameter(Mandatory = $true)] + $UserName, + [Parameter(Mandatory = $false)] + $ComparisonOrbVersion = "2021.4" + ) + + $logLead = (Get-LogLeadName) + $logFilePath = (Get-WebTestLogPath -BankUrl $BankUrl) + + $orbVersion = Get-OrbVersion + Write-Host "$loglead : orb version detected: $orbVersion" -ForegroundColor Green + $comparedSemVer = Compare-SemVer -Version1 $orbVersion -Version2 $ComparisonOrbVersion + Write-Host "$loglead : Compared SemVer Result: $comparedSemVer" -ForegroundColor Green + if($comparedSemVer -eq -1){ + Write-Host "$loglead : Detected orb version is LESS than $ComparisonOrbVersion" -ForegroundColor Green + Write-Host "$loglead : Setting Redis Key with 'E:' prefix" -ForegroundColor Green + $key = "E:{${BankUrl}}:System.String:${UserName}__AUTHENTICATION_NONCE__" + } else { + Write-Host "$loglead : Detected orb version is Not LESS than $ComparisonOrbVersion" -ForegroundColor Green + Write-Host "$loglead : Setting Redis Key without 'E:' prefix" -ForegroundColor Green + $key = "{${BankUrl}}:System.String:${UserName}__AUTHENTICATION_NONCE__" + } + + $guid = New-Guid + $nonce = "`"$guid`"" + + $redisConnectionString = Get-ConnectionString -name "RedisSetting" + + "$logLead : key : $key" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + "$logLead : nonce : $nonce" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + + try { + Invoke-RedisScript -ConnectionString $redisConnectionString -ScriptBlock { + $ttlTimespan = [System.TimeSpan]::new(0,10,0) + Add-RedisKey -Key $key -Value $nonce -TTL $ttlTimespan | Out-Null + } + } catch { + $_.exception | Tee-OutFile -Append -FilePath $logFilePath | Write-Error + } + + # do we need to do an audit to the audit table? + + return $guid +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Start-WebTests.ps1 b/Modules/Alkami.DevOps.Validations/Public/Start-WebTests.ps1 new file mode 100644 index 0000000..4e318e3 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Start-WebTests.ps1 @@ -0,0 +1,686 @@ +function Start-WebTests { +<# +.SYNOPSIS + Runs web tests on web-tier servers only. + +.DESCRIPTION + Site must be available in IIS. Keeps track of glogal pass/fail and returns a bool + +.PARAMETER LoginsJson + Json file containing logins to test with. Pulled from Get-LoginsToTest function. + +.PARAMETER CoreUrlsJson + Json file containing urls to test with. Pulled from Get-CoreUrlsToTest function. + +.PARAMETER SitesToSkip + A list (array) of sites to skip web tests on. Http/https will be trimmed +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory)] + [string]$LoginsJson, + [Parameter(Mandatory)] + [string]$CoreUrlsJson, + [Parameter(Mandatory = $false)] + [bool]$SkipAdmin = $false, + [Parameter(Mandatory = $false)] + [bool]$SkipAccountCount = $true, + [Parameter(Mandatory = $false)] + [bool]$SkipWidgetWarmUp = $true, + [Parameter(Mandatory = $false)] + [int]$MaximumSitesToTest = 20, + [Parameter(Mandatory = $false)] + [string[]]$SitesToSkip, + [Parameter(Mandatory = $false)] + [int]$SiteThreads = 4, + [Parameter(Mandatory = $false)] + [int]$WidgetThreads = 8 + ) + begin { + $previousGlobalVerbosity = $global:VerbosePreference + # Quiet the logs for now. + # $global:VerbosePreference = 'Continue' + } + process { + $logLead = (Get-LogLeadName) + $WebTestBaseName = "Start-WebTests" + (Reset-WebTestLogFolder -BaseLogFileName $WebTestBaseName) + $logFilePath = (Get-WebTestLogPath -LogName $WebTestBaseName) + + # Only run on Web-tier servers. + if(!(Test-IsWebServer)) { + "$logLead : Current server is not a Web-tier server. Skipping." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + return $true + } + + Import-Module -Name PsRedis -Global + + # Variables for setup + $logins = ($LoginsJson | ConvertFrom-Json) + $coreurls = ($CoreUrlsJson | ConvertFrom-Json) + + # Get all of the URLs in the file that are on this web server. + $siteNames = ((Get-IISSiteList).Bindings.Host).Where({!(Test-IsCollectionNullOrEmpty $logins.$_)}) | Sort-Object -Unique + $siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String) + "$logLead : Sites available to test:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + "$logLead : SitesToSkip: $SitesToSkip" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + if(!(Test-IsCollectionNullOrEmpty $SitesToSkip)) + { + # Trim any leading "http" nonsense, if necessary. + $SitesToSkip = $SitesToSkip | ForEach-Object { + try { + if ($_.StartsWith("http")) { + $site = $_ + [System.Uri]::new($_).Host + } else { $_ } + } + catch { + "$logLead : $site could not be parsed as a site to skip. It is being ignored." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + Resolve-Error + } + } + + $siteNames = $siteNames | Where-Object{$_ -notin $SitesToSkip} + "$logLead : Site Names after exclusions: $siteNames" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + } + + # If no sites found that match on this webserver, return. + if(Test-IsCollectionNullOrEmpty $siteNames) { + "$logLead : No sites to test on this web server!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + return $true + } + + # Limit the number of sites to test. + $siteNames = ($siteNames | Get-Random -Count $MaximumSitesToTest) + $siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String) + "$logLead : Limiting the number of sites to test to $MaximumSitesToTest. Actual number of sites under test is $($siteNames.Count)." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + "$logLead : Limited Site Names:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + $allTestResults = @() + + $StopWatch = [system.diagnostics.StopWatch]::StartNew() + "$logLead : Starting Web Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + if($SkipAdmin) { + "$logLead : Skipping Admin Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + } + if($SkipAccountCount) { + "$logLead : Skipping Account Count Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + } + if($SkipWidgetWarmUp) { + "$logLead : Skipping Site Widget Warm Up" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + } + + # Output some inputs + "$logLead : Maximum Sites To Test: $MaximumSitesToTest" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + "$logLead : Site Threads: $SiteThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + "$logLead : Widget Threads: $WidgetThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + # Iterate over the sites and get Synthetic accounts from $logins + $siteTestResult = Invoke-Parallel -Objects $siteNames -ReturnObjects -ThreadPerObject -NumThreads $SiteThreads -Arguments @($logins, $coreurls, $SkipAdmin, $SkipAccountCount, $SkipWidgetWarmUp, $WidgetThreads) -script { + Param( + $siteName, + $inputArguments + ) + try { + + $logLead = "[Invoke-Parallel `$siteNames ($siteName)]" + + $logFilePath = (Get-WebTestLogPath -BankUrl $siteName) + + $sb_logins = $inputArguments[0] + $sb_coreurls = $inputArguments[1] + $sb_skipAdmin = $inputArguments[2] + $sb_skipAccountCount = $inputArguments[3] + $sb_skipWidgetWarmUp = $inputArguments[4] + $sb_widgetThreads = $inputArguments[5] + + $siteLogins = @($sb_logins.$siteName) + + if (Test-IsCollectionNullOrEmpty $siteLogins) { + "$logLead : No logins to find, can't test the site!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + $allTestResults += (Test-Should -Result $adminResponse -Predicate ${function:Test-Passthrough} -Widget "Site Logins" -Route "Logins not available" -Login @{UrlSignature = $siteName; Username = "" } $true "There was no corresponding site entry for running this test against. Can not verify if the site is up or down.") + + continue + } + + $siteTestResults = @() + + if (!$sb_skipWidgetWarmUp) { + # Iterate over all of the widgets to warm them up. + $widgetStartResult = Invoke-Parallel -Objects $siteLogins.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($siteLogins.UrlSignature) -script { + Param( + $widgetName, + $inputArguments + ) + try { + $logLead = "[Invoke-Parallel `$siteLogins.Widgets ($widgetName)]" + + $urlSignature = $inputArguments[0] + $urlSignature = "https://$urlSignature/" + $status = "unknown" + + $logFilePath = (Get-WebTestLogPath -BankUrl $urlSignature) + + "$logLead : Widget Warmup $urlSignature$widgetName" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $result = (Invoke-Endpoint -Baseuri $urlSignature -Uri $widgetName) + $status = $result.StatusCode + + if ([string]::IsNullOrWhiteSpace($status)) { + $status = $result.LastError + } + + if ([string]::IsNullOrWhiteSpace($status)) { + $status = "Unknown" + } + + $resultWidgetObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Site Warmup" + ReturnValue = $true + ReturnMessage = "Status code: $status" + Username = $null + Url = $urlSignature + Route = $widgetName + Widget = "Site Warmup" + Parameters = $null + Runtime = $result.TotalRuntime + MachineName = (Get-FullyQualifiedServerName) + }) + return $resultWidgetObj + + } catch { + Write-Warning "$logLead : Failed to properly warm up widgets." + Write-Warning "$logLead : $_" + + $resultWidgetObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Site Warmup" + ReturnValue = $false + ReturnMessage = $_.Exception.Message + Username = $null + Url = $inputArguments[0] + Route = $widgetName + Widget = "Site Warmup" + Parameters = $null + Runtime = $null + MachineName = (Get-FullyQualifiedServerName) + }) + + return $resultWidgetObj + } + } + $siteTestResults += $widgetStartResult + + # Give the machine a break. + Start-Sleep -Seconds 3 + } + + # Each site base-url only has one admin site url to test + # Let's test that one first so we don't loop it when we loop users + # Cole says these will always be the same for a given FI in the above json + if (!$sb_skipAdmin) { + $adminSignature = ($siteLogins | Select-Object -First 1).AdminUrlSignature + + # Test admin variant of URL. + $fakeAdminLogin = @{UrlSignature = $adminSignature; Username = "" } # This is for logging purposes only + + $bankAdminUrl = "https://$($adminSignature)" + "$logLead : Testing Admin URL : $bankAdminUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $adminResponse = Invoke-Endpoint -Uri $bankAdminUrl + if ($adminResponse.HasErrors -and !($adminResponse.success)) { + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = $adminResponse.LastError + Username = $null + Url = $authUrl + Route = $null + Widget = "Admin Site Warmup" + Parameters = $null + Runtime = $adminResponse.TotalRuntime + MachineName = (Get-FullyQualifiedServerName) + }) + $siteTestResults += $resultObj + "$logLead : Error caught for Admin URL: $authUrl. Skipping tests!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + "$logLead : Exception: $($adminResponse.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + } + + if ($adminResponse.Success) { + "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 200) + $siteTestResults += $testResult + + "$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 'Content-Type' 'text/html') + $siteTestResults += $testResult + + # This is awful, but it's less awful than the rest of the options. TR + # Could I oneliner it? Yes. Would that be harder to maintain - or even read - in 6 months? Definitely. TR + $Content = $adminResponse.Result.Content + $newLine = [System.Environment]::NewLine + $lines = $Content.Split($newLine) + $iframe = $lines.Where( { $_ -match " + $WebSession = $authResponse.WebSession + + if ($authResponse.Success) { + "$logLead : Testing if we authenticated successfully..." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Authentication" -Route "Authenticated" -Login $login 200) + $successfulAuthentication = $testResult.ReturnValue + $siteTestResults += $testResult + + "$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Authentication" -Route "Authenticated" -Login $login 'Content-Type' 'text/html') + $siteTestResults += $testResult + + # Test if we landed on /Authentication/ page after attempting to login. + if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/Authentication") { + $successfulAuthentication = $false + + "$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + Start-Sleep -Seconds 3 + $retryCount = $retryCount + 1 + } else { + $stoploop = $true + } + } else { + "$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + Start-Sleep -Seconds 10 + $retryCount = $retryCount + 1 + } + } else { + $sbStopWatch.Stop() + # Break out because we tried 8 times without success. + $stoploop = $true + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = "Authentication failed after $maxRetries retries." + Username = $userName + Url = $login.UrlSignature + Route = "Authenticated" + Widget = "Authentication" + Parameters = $null + Runtime = [System.TimeSpan]::FromMilliseconds($StopWatch.ElapsedMilliseconds).ToString() + MachineName = (Get-FullyQualifiedServerName) + }) + $siteTestResults += $resultObj + } + } catch { + "$logLead : Exception for Auth URL: $authUrl. Exception:`n$($_.Exception.Message)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + Start-Sleep -Seconds 10 + $retryCount = $retryCount + 1 + } + } while ($stoploop -eq $false) + # ensure it got stopped + $sbStopWatch.Stop() + + # Test final auth attempt. + if (!$successfulAuthentication) { + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = "Authentication failed! We landed back on login." + Username = $userName + Url = $login.UrlSignature + Route = "Authenticated" + Widget = "Authentication" + Parameters = $null + Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString() + MachineName = (Get-FullyQualifiedServerName) + }) + $siteTestResults += $resultObj + } + + # Test if we authenticated. + if ($authResponse.Success) { + # Test if we landed on /AtLogin/ page + if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/AtLogin") { + $successfulAuthentication = $false + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = "Authentication failed! We landed on AtLogin. Has user logged in before? Is this a faux-synthetic?" + Username = $userName + Url = $login.UrlSignature + Route = "Authenticated" + Widget = "AtLogin" + Parameters = $null + Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString() + MachineName = (Get-FullyQualifiedServerName) + }) + $siteTestResults += $resultObj + } + + if ($successfulAuthentication) { + "$logLead : Testing widgets." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + + # Since webSession and Cookie collection can't be serialized going into the job, we must create a simple + # array with HashTable to hold the data and rebuild it inside the Invoke-Parallel. + $session = $WebSession + $cookies = @() + foreach ($cookie in $session.Cookies.GetCookies($authUrl)) { + $cookies += @{Name = $cookie.Name; Value = $cookie.Value; Domain = $cookie.Domain } + } + # Loop over the actual widget URL endpoints. Only the URLs where the widget name matches to the login widget. + $widgetUrlResult = Invoke-Parallel -Objects $login.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($sb_coreUrls, $login, $cookies, $bankUrl, $userAccountsCount, $userName, $sb_skipAccountCount) -script { + Param( + $widget, + $inputArguments + ) + + try { + $logLead = "[Invoke-Parallel `$login.Widgets ($widget)]" + + $widgetResults = @() + + $sb_coreUrls = $inputArguments[0] + $sb_widgetUrls = @() + if ($null -ne $sb_coreUrls.$widget) { + $sb_widgetUrls = @($sb_coreUrls.$widget) + } + $sb1_login = $inputArguments[1] + $sb1_WebSessionCookies = $inputArguments[2] + $sb1_bankUrl = $inputArguments[3] + $sb1_userAccountsCount = $inputArguments[4] + $sb1_userName = $inputArguments[5] + $sb1_skipAccountCount = $inputArguments[6] + + # Create a new session with the cookies array passed in. + $sb_WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + foreach ($cookie in $sb1_WebSessionCookies) { + # Create a new cookie with the data passed in. + $newCookie = New-Object System.Net.Cookie + $newCookie.Name = $cookie.Name + $newCookie.Value = $cookie.Value + $newCookie.Domain = $cookie.Domain + $sb_WebSession.Cookies.Add($newCookie) + } + + if (Test-IsCollectionNullOrEmpty $sb_widgetUrls) { + # We intentionally don't have anything to test here. + # This is okay. + # Also note the very special logging parameter because we don't have anything to do, so put this in the parent/site log + $logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl) + "$logLead : Nothing to test for $widget. This is cool." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + continue + } + + $logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl -Widget $widget) + "$logLead : Testing widget [$widget] routes [$($sb_widgetUrls -join ',')]" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + + foreach ($url in $sb_widgetUrls) { + $sbStopWatch2 = [System.Diagnostics.StopWatch]::StartNew() + if ([string]::IsNullOrWhiteSpace($url)) { + "$logLead : Found an empty url string for $widget. This indicates that either there was an empty string in the coreurls json array for this widget or something was misread. Continuing to the next route." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + continue + } + + "$logLead : Testing URL : $sb1_bankUrl$url" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $result = (Invoke-Endpoint -Baseuri $sb1_bankUrl -Uri $url -WebSession $sb_WebSession) + if ($result.HasErrors -and !($result.success)) { + $sbStopWatch2.Stop() + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = $result.LastError + Username = $sb1_userName + Url = $sb1_bankUrl + Route = $url + Widget = $widget + Parameters = $null + Runtime = $result.TotalRuntime + MachineName = (Get-FullyQualifiedServerName) + }) + $widgetResults += $resultObj + "$logLead : Could not test $sb1_bankUrl$url for $widget - Is the widget installed?" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + "$logLead : Exception: $($result.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning + } + + if ($result.Success) { + $sbStopWatch2.Stop() + $runtime = $sbStopWatch2.Elapsed.ToString("ss\.fffffff") + $status = $result.StatusCode + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $true + ReturnMessage = "Status code: $status" + Username = $sb1_userName + Url = $sb1_bankUrl + Route = $url + Widget = $widget + Parameters = $null + Runtime = $runtime + MachineName = (Get-FullyQualifiedServerName) + }) + $widgetResults += $resultObj + # Only check the account count if the path is MyAccountsV2. + if (!$sb1_skipAccountCount -and $url -eq "/MyAccountsV2/") { + "$logLead : Url is /MyAccountsV2/, Running test Test-HaveAccountCount." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveAccountCount} -Widget $widget -Route $url -Login $sb1_login $sb1_userAccountsCount) + $widgetResults += $testResult + } + + "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveStatusCode} -Widget $widget -Route $url -Login $sb1_login 200) + $widgetResults += $testResult + + # Commented out for now. Sub-paths may return JSON, so this test fails. + #$testResult = (Test-Should -Result $result -Predicate ${function:Test-HaveResponseHeader} 'Content-Type' 'text/html;') + #$siteTestResults += $testResult + + # If the URL ends with a '/', run the test. + if (($url -split '/')[-1].Length -eq 0) { + "$logLead : Running test Test-HaveContentThatMatches." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose + $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveContentThatMatches} -Widget $widget -Route $url -Login $sb1_login $url) + $widgetResults += $testResult + } + } + } + + return $widgetResults + } catch { + Write-Warning "$logLead : Exception occurred when testing widgets." + Write-Warning "$logLead : $_" + + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = $_.Exception.Message + Username = $sb1_userName + Url = $sb1_bankUrl + Route = $null + Widget = $widget + Parameters = $null + Runtime = $null + MachineName = (Get-FullyQualifiedServerName) + }) + + $widgetResults += $resultObj + return $widgetResults + } + } + $siteTestResults += $widgetUrlResult + } + } + } + + # foreach site result, write the aggregate times out by the widget + $hash = @{} + $maxKeyLength = 0 + foreach ($result in $siteTestResults) { + # Can't calculate runtimes if you don't have them + if ([string]::IsNullOrWhiteSpace($result.Runtime)) { + continue + } + try { + $widgetName = $result.Widget + $runtime = $result.Runtime + $runtimeSplit = $runtime.Split(':') + switch ($runtimeSplit.Length) { + 3 { $runtime = $runtime; break; } + 2 { $runtime = "00:$runtime"; break; } + 1 { $runtime = "00:00:$runtime"; break; } + } + $runtime = [System.TimeSpan]::Parse($runtime) + if ($null -eq $hash.$widgetName) { + $hash.$widgetName = [System.TimeSpan]::new(0) + } + $hash.$widgetName += $runtime + if ($widgetName.Length -gt $maxKeyLength) { + $maxKeyLength = $widgetName.Length + } + } catch { + Write-Warning "$logLead : Couldn't parse the runtime [$($result.Runtime)] for widget [$($result.Widget)]" + Resolve-Error + } + } + + $maxKeyLength += 2 + $siteUrl = $siteTestResults[0].Url + + "$logLead : Test runtime results for $siteUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + foreach ($key in $hash.Keys) { + "$siteUrl - $($key.PadLeft($maxKeyLength)) - $($hash.$key)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + } + + return $siteTestResults + } catch { + Write-Warning "$logLead : Exception occurred while running tests." + Write-Warning "$logLead : $_" + + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = "Invoke-Endpoint" + ReturnValue = $false + ReturnMessage = $_.Exception.Message + Username = $sb1_userName + Url = $sb1_bankUrl + Route = $null + Widget = $widget + Parameters = $null + Runtime = $null + MachineName = (Get-FullyQualifiedServerName) + }) + + $siteTestResults += $resultObj + return $siteTestResults + } + } + + $allTestResults += @($siteTestResult) + + # Log all test results at once for a pretty table + $resultMessage = (Format-Table -AutoSize -Property @( + @{Label = "Should Test"; Expression = {$_.FunctionName.Replace("Test-","")}; Alignment = "Left"}, + @{Label = "Passed?"; Expression = {$_.ReturnValue}; Alignment = "Left"}, + @{Label = "Url"; Expression = {$_.Url}; Alignment = "Left"}, + @{Label = "Route"; Expression = {$_.Route}; Alignment = "Left"}, + @{Label = "Result Message"; Expression = {$_.ReturnMessage}; Alignment = "Left";} + @{Label = "Runtime"; Expression = {$_.Runtime}; Alignment = "Left";} + ) -InputObject $allTestResults) | Out-String -Width 300 + "`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + $resultMessage.Trim() | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + "`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + <# + Commented out but keeping so we can bring this work into a next cycle. + Per discussion with Brent, Justin, and Cole + The two long javascript and css strings should be put into a ride-along file that we get-content and use inline below for maintainability. + This was demoable but not usable from the agent. Future work would be to generate this on the agent along with parsing all the results + On the agent so we can do BuildProblems on the failing lines as well. + + $htmlParams = @{ + Title = "Start-WebTests :: All Results" + PreContent = "

All Results

" + PostContent = "Generated $([System.DateTime]::Now.ToString())" + } + $allTestResults | ConvertTo-Html @htmlParams | Out-File .\all.html + #> + + $passedAllTests = $allTestResults.Where({ $_.returnValue -eq $false }).Count -eq 0 + "$logLead : Passed all tests? $passedAllTests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + $StopWatch.Stop() + $totalHours = $StopWatch.Elapsed.Hours + $totalMinutes = $StopWatch.Elapsed.Minutes + $totalSecs = $StopWatch.Elapsed.Seconds + + "$logLead : Web tests ran for a total of $totalHours hours, $totalMinutes minutes, $totalSecs seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host + + return $passedAllTests + } + end { + $global:VerbosePreference = $previousGlobalVerbosity + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-HaveAccountCount.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-HaveAccountCount.ps1 new file mode 100644 index 0000000..fa32a09 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-HaveAccountCount.ps1 @@ -0,0 +1,43 @@ +function Test-HaveAccountCount { + <# + .SYNOPSIS + Test the count of accounts for a user/url. + + .DESCRIPTION + Intended to be used in conjunction with Test-Should function. + + .PARAMETER result + The result of the Invoke-WebRequest containing Html to test against. + + .PARAMETER expect + The number of accounts expected to be in the $result. + + .EXAMPLE + $passedTest = (Test-Should -Result $webResponse -Predicate ${function:Test-HaveAccountCount} 3) + + .NOTES + Returns a bool of success and/or error message. + #> + [CmdletBinding()] + [OutputType([System.Boolean])] + [OutputType([System.String])] + param( + $result, + $expect + ) + + try { + $html = $result.Content + $accountsCount = ([regex]::Matches($html, 'class="account ')).count + + if($expect -ne $accountsCount) { + $false + "Number of accounts doesn't match. Expected $expect but found $accountsCount." + } else { + $true + } + } catch { + $false + "Error occured inside test! Exception: $_" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-HaveContentThatMatches.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-HaveContentThatMatches.ps1 new file mode 100644 index 0000000..ac7786b --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-HaveContentThatMatches.ps1 @@ -0,0 +1,41 @@ +function Test-HaveContentThatMatches { +<# +.SYNOPSIS + Tests that the content in $result matches partially the value in $pattern + +.DESCRIPTION + Intended to be used in conjunction with Test-Should function. + +.PARAMETER result + Result of Invoke-WebRequest containing Html to test against. + +.PARAMETER pattern + RegEx pattern to test the $result with. Performs $result.Content -match $pattern + +.EXAMPLE + $passedTest = (Test-Should -Result $webResponse -Predicate ${function:Test-HaveContentThatMatches} "/DashboardV2") + +.NOTES + Returns a bool of success and/or error message. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + [OutputType([System.String])] + param( + $result, + [regex]$pattern + ) + + try { + if (-not($result.Content -match $pattern)) { + $false + "returned content that did not match $pattern" + } + else { + $true + } + } catch { + $false + "Error occured inside test! Exception: $_" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-HaveResponseHeader.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-HaveResponseHeader.ps1 new file mode 100644 index 0000000..8136d3b --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-HaveResponseHeader.ps1 @@ -0,0 +1,50 @@ +function Test-HaveResponseHeader { +<# +.SYNOPSIS + Tests that the response header in $result contains the value in $headername/$headervalue + +.DESCRIPTION + Intended to be used in conjunction with Test-Should function. + +.PARAMETER result + Result of Invoke-WebRequest containing Html to test against. + +.PARAMETER headername + Response header name to test + +.PARAMETER headervalue + Response header value to test + +.EXAMPLE + $passed = (Test-Should -Result $webResponse -Predicate ${function:Test-HaveResponseHeader} 'Content-Type' 'text/html;') + +.NOTES + Returns a bool of success and/or error message. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + [OutputType([System.String])] + param( + $result, + $headername, + $headervalue + ) + + try { + if (-not($result.Headers.Keys.Contains($headername))) { + $false + "did not return a response header '$headername'" + } else { + $header = $result.Headers[$headername] + if (-not($header.Contains($headervalue))) { + $false + "returned header '$headername=$header' which does not contain expected '$headervalue'" + } else { + $true + } + } + } catch { + $false + "Error occured inside test! Exception: $_" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-HaveStatusCode.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-HaveStatusCode.ps1 new file mode 100644 index 0000000..3a6b59f --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-HaveStatusCode.ps1 @@ -0,0 +1,42 @@ +function Test-HaveStatusCode { +<# +.SYNOPSIS + Tests that the $result has status code in $expect + +.DESCRIPTION + Intended to be used in conjunction with Test-Should function. + +.PARAMETER result + Result of Invoke-WebRequest containing Html to test against. + +.PARAMETER expect + Status code to expect in $result + +.EXAMPLE + $passedTest = (Test-Should -Result $webRequest -Predicate ${function:Test-HaveStatusCode} 200) + +.NOTES + Returns a bool of success and/or error message. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + [OutputType([System.String])] + param( + $result, + $expect + ) + + $returnedCode = $result.StatusCode -as [int] + + try { + if (-not($returnedCode -eq $expect)) { + $false + "returned wrong status code: $returnedCode. Expected: $expect" + } else { + $true + } + } catch { + $false + "Error occured inside test! Exception: $_" + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-Passthrough.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-Passthrough.ps1 new file mode 100644 index 0000000..5ac8401 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-Passthrough.ps1 @@ -0,0 +1,33 @@ +function Test-Passthrough { + <# + .SYNOPSIS + Returns the first value in the arguments list + + .DESCRIPTION + Mostly useful for forcing a test result to true or false + + .PARAMETER result + Ignored entirely + + .PARAMETER expect + Returned value + + .PARAMETER message + Optional message to be returned + + .EXAMPLE + $passedTest = (Test-Should -Result $webRequest -Predicate ${function:Test-Passthrough} $false "Test was invalid") + + .NOTES + Returns the value of $expect + #> + [cmdletbinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", '', Justification="This is a test function; it's supposed to have an unused param", Scope = "Function")] + param( + $result, + $expect, + $message + ) + $expect + $message +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/Public/Test-Should.ps1 b/Modules/Alkami.DevOps.Validations/Public/Test-Should.ps1 new file mode 100644 index 0000000..97c819c --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/Public/Test-Should.ps1 @@ -0,0 +1,70 @@ +function Test-Should { + <# + .SYNOPSIS + Function that calls validation functions and collects the output for full review. + + .DESCRIPTION + Used in conjunction with other Test-* functions in this module. + + .PARAMETER result + Result of Invoke-WebRequest containing Html to test against. + + .PARAMETER predicate + Script block to execute. Usually a single function in the format of ${function:Test-HaveStatusCode} + + .PARAMETER Widget + The widget under test, for better detail logging + + .PARAMETER Route + The route under test, for better detail logging + + .PARAMETER Login + The Login object used to represent the user under test, for better detail logging + + .PARAMETER _args + Remaining arguments that the $predicate function needs. + + .EXAMPLE + $passedTest = (Test-Should -Result $webRequest -Predicate ${function:Test-HaveStatusCode} 200) + $passedTest = (Test-Should -Result $webRequest -Predicate ${function:Test-HaveResponseHeader} 'Content-Type' 'text/html;') + + .NOTES + Writes out the result of the $predicate test function in Format-Table in Verbose mode and returns the status of the function test. + #> + [cmdletbinding()] + param( + [Parameter(Mandatory)] + $result, + [Parameter(Mandatory)] + [scriptblock]$predicate, + [Parameter(Mandatory)] + [string]$Widget, + [Parameter(Mandatory)] + [string]$Route, + [Parameter(Mandatory)] + $Login, + [Parameter(Mandatory, ValueFromRemainingArguments)] + $_args + ) + + process { + $StopWatch = [System.Diagnostics.StopWatch]::StartNew() + $isOk, $err = & $predicate $result @_args + $StopWatch.Stop() + + $resultObj = New-Object PSObject -Property $([ordered]@{ + FunctionName = $predicate.Ast.Name + ReturnValue = $isOk + ReturnMessage = $err + Username = $login.Username + Url = $login.UrlSignature + Route = $Route.TrimStart('/') + Widget = $widget + Parameters = (@($_args) -Join ',') + Runtime = $StopWatch.Elapsed.ToString("ss\.fffffff") + MachineName = (Get-FullyQualifiedServerName) + }) + + return $resultObj + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/tools/chocolateyInstall.ps1 b/Modules/Alkami.DevOps.Validations/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..173e3d2 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/tools/chocolateyInstall.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + $resourcesFolder = (Join-Path $parentPath "Resources") + if (Test-Path $resourcesFolder) { + + Write-Host "Copying resources folder for module $id to [$targetModuleVersionPath]" + Copy-Item $resourcesFolder -Destination $targetModuleVersionPath -Recurse -Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.DevOps.Validations/tools/chocolateyUninstall.ps1 b/Modules/Alkami.DevOps.Validations/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..edd4b55 --- /dev/null +++ b/Modules/Alkami.DevOps.Validations/tools/chocolateyUninstall.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.csproj b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.csproj new file mode 100644 index 0000000..2f662e5 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.csproj @@ -0,0 +1,120 @@ + + + + + Debug + AnyCPU + {AD323736-DE44-42D9-A574-F61A42C69CF8} + Library + Properties + Alkami.Ops.Certificates + Alkami.Ops.Certificates + v4.7.2 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + x64 + + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + + + + + + ..\packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.1.0\lib\net4\System.Management.Automation.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + {fa9745dd-68ac-4194-9c33-acf19411d357} + Alkami.Ops.Common + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.nuspec b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.nuspec new file mode 100644 index 0000000..83dbbbc --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.nuspec @@ -0,0 +1,20 @@ + + + + Alkami.Ops.Certificates + $version$ + Alkami Technology + Alkami Technology + false + Alkami.Ops.Certificates + + + + + + + + + + + diff --git a/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.psd1 b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.psd1 new file mode 100644 index 0000000..79c4e9b --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Alkami.Ops.Certificates.psd1 @@ -0,0 +1,9 @@ +@{ + RootModule = 'Alkami.Ops.Certificates' + ModuleVersion = '2.3.1' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2019 Alkami Technologies, Inc.. All rights reserved.' + Description = 'cmdlet Certificate functions' + PowerShellVersion = '5.0' + DotNetFrameworkVersion = '4.7.2' +} diff --git a/Modules/Alkami.Ops.Certificates/AlkamiManifest.xml b/Modules/Alkami.Ops.Certificates/AlkamiManifest.xml new file mode 100644 index 0000000..3272993 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.Ops.Certificates + SREModule + + + Production + + diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/GetAllThumbprintsInStores.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/GetAllThumbprintsInStores.cs new file mode 100644 index 0000000..2c49431 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/GetAllThumbprintsInStores.cs @@ -0,0 +1,52 @@ +using Alkami.Ops.Certificates.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.cmdlets +{ + /// + /// Gets the certificate thumbprints for every certificate in the local certificate store. + /// + [Cmdlet("Get", "AllThumbprintsInStores")] + [OutputType(typeof(List))] + public class GetAllThumbprintsInStores : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + [ValidateSet("all", "my", "CertificateAuthority", "root", "trustedpeople")] + public string certStore; + + protected override void ProcessRecord() + { + // Think of this like a return, but it returns the object to the powershell output stream, like write-output + WriteObject(GetName(this.certStore)); + } + + public IEnumerable GetName(string certStore) + { + var storesToSearch = new string[4]; + if (certStore == "all") + { + storesToSearch = new string[] { "my", "CertificateAuthority", "root", "trustedpeople" }; + } + else + { + storesToSearch = new string[] { certStore }; + } + + X509Certificate2Collection allCertificates = new X509Certificate2Collection(); + foreach (var storeString in storesToSearch) + { + StoreName.TryParse(storeString, true, out StoreName storeName); + + allCertificates.AddRange(Common.Cryptography.CertificateHelper.GetAllCertificates(storeName, StoreLocation.LocalMachine)); + } + + IEnumerable thumbprints = allCertificates.ToList() + .Select(cert => cert.Thumbprint); + + return thumbprints; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertNameByThumbprint.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertNameByThumbprint.cs new file mode 100644 index 0000000..badc89a --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertNameByThumbprint.cs @@ -0,0 +1,51 @@ +using Alkami.Ops.Common.Cryptography; +using System; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.cmdlets +{ + /// + /// Gets the SimpleName for a given Thumbprint + /// + [Cmdlet("Get", "CertNameByThumbprint")] + [OutputType(typeof(string))] + public class GetCertNameByThumbprint : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string thumbprint; + + [Parameter(Position = 1, Mandatory = true)] + [ValidateSet("all", "my", "CertificateAuthority", "root", "trustedpeople")] + public string certStore; + + protected override void ProcessRecord() + { + GetName(this.thumbprint, this.certStore); + } + + public void GetName(string thumbprint, string certStore = "all") + { + var storesToSearch = new string[4]; + if (certStore == "all") + { + storesToSearch = new string[] { "my", "CertificateAuthority", "root", "trustedpeople" }; + } + else + { + storesToSearch = new string[] { certStore }; + } + + foreach (var storeString in storesToSearch) + { + StoreName.TryParse(storeString, true, out StoreName storeName); + + X509Certificate2 certificate = CertificateHelper.FindCertificateByThumbprint(thumbprint, storeName, StoreLocation.LocalMachine, "localhost"); + if (certificate != null) + { + WriteObject($"Found {certificate.GetNameInfo(X509NameType.SimpleName, false)} in {storeString}"); + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertThumbprintByName.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertThumbprintByName.cs new file mode 100644 index 0000000..03993ef --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/GetCertThumbprintByName.cs @@ -0,0 +1,57 @@ +using System; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.cmdlets +{ + /// + /// Gets the thumbprint value for a given cert Name + /// + [Cmdlet("Get", "CertThumbprintByName")] + [OutputType(typeof(string))] + public class GetCertThumbprintByName : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string CertName; + + [Parameter(Position = 1, Mandatory = true)] + [ValidateSet("all", "my", "CertificateAuthority", "root", "trustedpeople")] + public string CertStore; + + + protected override void ProcessRecord() + { + GetThumbprint(CertName, CertStore); + } + + public void GetThumbprint(string CertName, string CertStore) + { + var StoresToSearch = new string[4]; + if (CertStore == "all") + { + StoresToSearch = new string[] {"my", "CertificateAuthority", "root", "trustedpeople"}; + } + else + { + StoresToSearch = new string[] {CertStore}; + } + + Console.WriteLine($"Searching {CertStore} Store(s) for {CertName}"); + foreach (var storeString in StoresToSearch) + { + StoreName.TryParse(storeString, true, out StoreName storeName); + X509Certificate2 certificate = + Common.Cryptography.CertificateHelper.FindCertificatebySubjectOrSAN(CertName, storeName, + StoreLocation.LocalMachine); + if (certificate != null) + { + Console.WriteLine($"found {certificate.Thumbprint} in {storeString}"); + } + else + { + Console.WriteLine($"cert not found in {storeName}"); + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/GetUntrackedCertificates.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/GetUntrackedCertificates.cs new file mode 100644 index 0000000..a591d96 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/GetUntrackedCertificates.cs @@ -0,0 +1,61 @@ +using Alkami.Ops.Certificates.Utilities; +using Alkami.Ops.Common.Cryptography; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.Cmdlets +{ + [Cmdlet("Get", "UntrackedCertificates")] + [OutputType(typeof(string))] + public class GetUntrackedCertificates : Cmdlet + { + [Parameter(Position = 0, Mandatory = false)] + public string thumbprintsFilePath { get; set; } = @"C:\Tools\CertificateManagement\TrackedThumbprints\"; + + private readonly string[] storeTypes = new string[] { "personal", "ia", "root", "trustedpeople" }; + + /// + /// Entry point method. + /// + protected override void ProcessRecord() + { + var untrackedJsonFilePath = Path.Combine(thumbprintsFilePath, "untracked.json"); + if (!Directory.Exists(thumbprintsFilePath) && !File.Exists(untrackedJsonFilePath)) + { + Console.WriteLine("No un-tracked certificates at the specified location. Returning."); + return; + } + + var untrackedCertDetails = JsonConvert.DeserializeObject>(File.ReadAllText(untrackedJsonFilePath)); + var localCerts = new X509Certificate2Collection(); + + // Get all certs + foreach (var storeName in this.storeTypes) + { + var certStore = Extensions.GetStoreNameByFolderName(storeName); + localCerts.AddRange(CertificateHelper.GetAllCertificates(certStore, StoreLocation.LocalMachine)); + } + + var untrackedLocalCerts = new X509Certificate2Collection(); + + foreach (var untrackedCert in untrackedCertDetails) + { + // It's possible that certs will be duplicated in multiple stores. Just pull the first. + var tempCerts = localCerts.Find(X509FindType.FindByThumbprint, untrackedCert.Key, false); + if (tempCerts.Count > 0) + { + untrackedLocalCerts.Add(tempCerts[0]); + } + } + + var untrackedCertList = untrackedLocalCerts.ToList(); + + WriteObject(untrackedCertList.Select(s => new KeyValuePair(s.Thumbprint, s.GetNameInfo(X509NameType.SimpleName, false)))); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesFromSecretServer.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesFromSecretServer.cs new file mode 100644 index 0000000..dc57e26 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesFromSecretServer.cs @@ -0,0 +1,487 @@ +using Alkami.Ops.Certificates.Data; +using Alkami.Ops.Certificates.SecretServer; +using Alkami.Ops.Certificates.SecretServer.Models; +using Alkami.Ops.Certificates.Utilities; +using Alkami.Ops.Common.Cryptography; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.ServiceProcess; + +namespace Alkami.Ops.Certificates +{ + /// + /// Downloads certificates for the current server from the appropriate Secret Server MachineSecrets folder if it exists. + /// + [Cmdlet("Import", "CertificatesFromSecretServer")] + [OutputType(typeof(string))] + public class ImportCertificatesFromSecretServer : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string SecretUsername { get; set; } + + [Parameter(Position = 1, Mandatory = true)] + public string SecretPassword { get; set; } + + [Parameter(Position = 2, Mandatory = false)] + public string GrantUserGmsaPrefix { get; set; } + + [Parameter(Position = 3, Mandatory = false)] + public string SecretSite { get; set; } = "https://alkami.secretservercloud.com"; + + [Parameter(Position = 4, Mandatory = false)] + public string MachineSecretFolder { get; set; } = "ops.deployment-CertApi/MachineSecrets"; + + [Parameter(Position = 5, Mandatory = false)] + public string thumbprintsFilePath { get; set; } = @"C:\Tools\CertificateManagement\TrackedThumbprints\"; + + private readonly string[] storeTypes = new string[] { "personal", "ia", "root", "trustedpeople" }; + private string[] extensions = new string[] { ".pfx", ".cer" }; + + /// + /// Entry point method. + /// + protected override void ProcessRecord() + { + string downloadPathTempPath = Path.Combine(Path.GetTempPath(), "CertificateImport"); + try + { + // Create a temp path to stage certs + if (Directory.Exists(downloadPathTempPath)) + { + Extensions.ClearDirectory(downloadPathTempPath); + } + else + { + Directory.CreateDirectory(downloadPathTempPath); + } + + // Create a folder to record installed, but untracked certs. + if (!Directory.Exists(this.thumbprintsFilePath)) + { + Directory.CreateDirectory(this.thumbprintsFilePath); + } + + ImportCertificates(downloadPathTempPath, this.GrantUserGmsaPrefix, this.MachineSecretFolder, this.SecretPassword, this.SecretSite, this.SecretUsername); + } + finally + { + if (Directory.Exists(downloadPathTempPath)) + { + Directory.Delete(downloadPathTempPath, true); + } + } + } + + /// + /// Imports certificates to the machine from the MachineSecrets folder. + /// + /// Path where secret zips will be stored temporarily + /// Pod prefix for the GMSA accounts + /// Name of the folder in Secret Server from which we're downloading secrets + /// Password used to authenticate with Secret Server + /// Secret Server URI + /// Username used to authenticate with Secret Server + private void ImportCertificates(string downloadPath, string gmsaPrefix, string machineSecretFolder, string secretPassword, string secretSite, string secretUsername) + { + Console.WriteLine("Importing certificates to server from the secret server."); + + // Get the environment properties of the current server. + var serverInfo = Extensions.GetServerInfo("localhost"); + + if (serverInfo == null) + { + Console.WriteLine("Environment properties could not be successfully read from the machine config. Exiting..."); + return; + } + + // Create secret server client. + using (var client = new SecretServerClient(secretSite, secretUsername, secretPassword)) + { + List preliminarySecretList = GetSecretsForServer(machineSecretFolder, serverInfo, client); + + // Load file hashes from all files in thumbprintsFilePath excepting untracked.json + var secretInfoFiles = Directory.GetFiles(thumbprintsFilePath).Where(s => s.IndexOf("untracked.json") == -1).Select(s => s); + var zipHashes = new List(); + + foreach (var file in secretInfoFiles) + { + var fileJson = File.ReadAllText(file); + + var fileData = JsonConvert.DeserializeObject(fileJson); + zipHashes.Add(fileData.FileHash); + } + + var unchangedSecretIds = new List(); + foreach (var secret in preliminarySecretList) + { + foreach (var hash in zipHashes) { + if (!client.DetectChanges(secret.ID, hash).Result) + { + unchangedSecretIds.Add(secret.ID); + Console.WriteLine($"Zip file has not changed for secret {secret.Name} from folder ID {secret.FolderID}. Skipping download."); + } + } + + if (!unchangedSecretIds.Contains(secret.ID)) + { + Console.WriteLine($"Downloading secret '{secret.Name}' with ID {secret.ID} from folder ID {secret.FolderID}"); + } + } + + var finalSecretList = preliminarySecretList.Where(s => !unchangedSecretIds.Contains(s.ID)); + + // If there are no secrets to download, return! + if (finalSecretList == null && !finalSecretList.Any()) + { + Console.WriteLine("Could not locate any certificate secrets to download and install. Exiting..."); + return; + } + + // Write debug info about which secrets are being downloaded. + WriteVerbose($"Found {finalSecretList.Count()} secrets to download."); + + // Fetch the full secret. GetSecretsByFolder only fetches the top-level secret info, and not the data in the secret. + finalSecretList = finalSecretList.Select(secret => client.GetSecretByID(secret.ID).GetAwaiter().GetResult()).ToList(); + + // Download all the machine secrets, and install them + var certificateZipsAndPasswords = new List<(string zipPath, string importPassword, string secretName)>(); + foreach (var secret in finalSecretList) + { + var zipFilePath = Path.Combine(downloadPath, $"{Guid.NewGuid()}.zip"); + var downloadSuccessful = client.DownloadFile(zipFilePath, secret).GetAwaiter().GetResult(); + if (downloadSuccessful) + { + certificateZipsAndPasswords.Add((zipFilePath, secret["Import Password"], secret.Name)); + } + else + { + throw new Exception($"Failed to download secret {secret.Name}"); + } + } + + // Determine the users to grant rights to the certificates to. + var grantUsers = GetUsersToGrantRightsTo(serverInfo.MicroUser, serverInfo.DatabaseUser, gmsaPrefix); + + // Unzip all of the cert zips, import the certs, and track which ones we just installed. + var unzipPath = Path.Combine(downloadPath, "Certificates"); + var thumbprintsFromSecret = new List(); + foreach (var zipAndPassword in certificateZipsAndPasswords) + { + var unzipOutputDirectory = Path.Combine(unzipPath, Path.GetFileNameWithoutExtension(zipAndPassword.zipPath)); + var zipHash = ""; + + using (var fileStream = File.OpenRead(zipAndPassword.zipPath)) + { + zipHash = Extensions.GetMd5HashString(fileStream); + } + + System.IO.Compression.ZipFile.ExtractToDirectory(zipAndPassword.Item1, unzipOutputDirectory); + ImportCertificatesToLocalMachine(serverInfo, unzipOutputDirectory, grantUsers, zipAndPassword.importPassword); + // Get list of new tracked certs and append to a total list of all tracked certs + var certificateFiles = Directory.GetFiles(unzipOutputDirectory, "*", SearchOption.AllDirectories); + foreach (string certificateFile in certificateFiles) + { + var tempCertificate = new X509Certificate2(certificateFile, zipAndPassword.importPassword); + thumbprintsFromSecret.Add(tempCertificate.Thumbprint); + tempCertificate.Dispose(); + } + + var secretZipInfo = new SecretZipInfo() { CertificateThumbprints = thumbprintsFromSecret, FileHash = zipHash }; + + // Write certs pulled from Secret to file + string thumprintsFileName = this.thumbprintsFilePath + zipAndPassword.secretName + ".json"; + + File.WriteAllText(thumprintsFileName, JsonConvert.SerializeObject(secretZipInfo)); + } + + // Compare local certs against list of all tracked certs. + TrackUnregisteredCerts(thumbprintsFromSecret); + + Console.WriteLine("Certificate import complete."); + } + } + + private List GetSecretsForServer(string machineSecretFolder, ServerInfo serverInfo, SecretServerClient client) + { + // Determine the base folder for the environment type, the common folder for that environment, and the folder for the specific environment. + var baseFolderPath = Path.Combine(machineSecretFolder, serverInfo.EnvironmentType); + var commonFolderPath = Path.Combine(baseFolderPath, "Common"); + var environmentFolderPath = Path.Combine(baseFolderPath, serverInfo.EnvironmentName); + var commonFolder = client.GetFolder(commonFolderPath); + var environmentFolder = client.GetFolder(environmentFolderPath); + + // Determine which secrets we need to download from the machine folders. + var desiredServerTypes = new string[] { "all", serverInfo.ServerType.ToLower() }; + + WriteVerbose($"Determined that ServerType is {serverInfo.ServerType}."); + + // Download the 4 secrets relevant to this environment if the folders/secrets exist. + // EnvironmentName / (Web|App)&(All) + // Common / (Web|App)&(All) + var secretsToDownload = new List(); + if (commonFolder != null) + { + var commonSecrets = client.GetSecretsByFolder(commonFolder); + foreach (var secret in commonSecrets) + { + // all secrets must have unique names, this sanitizes back to normal names + secret.Name = secret.Name.Split('-')[0]; + } + secretsToDownload = commonSecrets.Where(secret => desiredServerTypes.Contains(secret.Name.ToLower())).ToList(); + } + + // Check for pod specific secrets. If they exist, concat. Otherwise, just use common as pulled above. + if (environmentFolder != null) + { + var secrets = client.GetSecretsByFolder(environmentFolder); + foreach (var secret in secrets) + { + secret.Name = secret.Name.Split('-')[0]; + } + + var envSecretsToDownload = secrets.Where(secret => desiredServerTypes.Contains(secret.Name.ToLower())).ToList(); + if (!secretsToDownload.Any()) + { + secretsToDownload = envSecretsToDownload; + } + else + { + secretsToDownload.Concat(envSecretsToDownload); + } + } + + return secretsToDownload; + } + + /// + /// Take a list of thumbprints and compare it with those currently installed on the machine where this is run. + /// + /// List of certificate thumbprints. + private void TrackUnregisteredCerts(List trackedThumbprints) + { + var localCerts = new Dictionary>(); + // Get cert thumbprints from local store + foreach (var storeName in this.storeTypes) + { + var certStore = Extensions.GetStoreNameByFolderName(storeName); + localCerts.Add(storeName, CertificateHelper.GetAllCertificates(certStore, StoreLocation.LocalMachine).ToList().Select(c => c.Thumbprint).ToList()); + } + + // Compare 2 lists of certs (from secret, and from local store). + var allLocalCerts = localCerts.SelectMany(c => c.Value); + var allUntrackedCerts = CompareCertThumbprints(allLocalCerts, trackedThumbprints); + // Write untracked certs to file + File.WriteAllText(this.thumbprintsFilePath + "untracked.json", JsonConvert.SerializeObject(allUntrackedCerts)); + } + + /// + /// Takes two lists of thumbprints and returns a list of untracked certificats and when they were found. + /// + /// Collection of certificates from local store + /// Collection of known managed certificates + /// Dictionary of all untracked thumbprints paired with the first time they were found. + private Dictionary CompareCertThumbprints(IEnumerable localCerts, IEnumerable managedCertThumbprints) + { + var untrackedCerts = localCerts.Except(managedCertThumbprints); + + var untrackedJsonFilePath = this.thumbprintsFilePath + "untracked.json"; + var returnCerts = new Dictionary(); + if (File.Exists(untrackedJsonFilePath)) + { + var legacyUntrackedJsonFile = File.ReadAllText(untrackedJsonFilePath); + var legacyUntrackedCerts = JsonConvert.DeserializeObject>(legacyUntrackedJsonFile); + + var legacyUntrackedThumbprints = legacyUntrackedCerts.Select(u => u.Key); + + var newUntrackedCerts = untrackedCerts.Except(legacyUntrackedThumbprints).ToDictionary(c => c, c => DateTime.Now); + + foreach (var cert in newUntrackedCerts) + { + // This should never fail, because we're only adding certs we didn't find above. + if (!legacyUntrackedCerts.TryAdd(cert.Key, cert.Value)) + { + WriteWarning("Warning: Somehow found duplicate untracked certs with thumbprint " + cert.Key + " when writing to a file. WTF?"); + } + } + + returnCerts = legacyUntrackedCerts; + } + else + { + // Create untracked file with all untracked certs. All timestamps should be DateTime.now + returnCerts = untrackedCerts.ToDictionary(c => c, c => DateTime.Now); + } + + return returnCerts; + } + + /// + /// Imports a certificate directory with standard ia/personal/root/trustedpeople folders into the local machine. + /// + /// + /// + private void ImportCertificatesToLocalMachine(ServerInfo server, string importFolder, string[] grantUsers, string password) + { + WriteVerbose($"Importing certificates to '{importFolder}' with rights granted to users '{string.Join(",", grantUsers)}'"); + + foreach (var store in this.storeTypes) + { + // Determine the folder of certs to import for the store type. + var folderPath = Path.Combine(importFolder, store); + if (!Directory.Exists(folderPath)) + { + continue; + } + + // Grab all of the certs. + var certificates = Directory.GetFiles(folderPath) + .Where(file => extensions.Contains(Path.GetExtension(file).ToLower())); // Filter to .pfx, and .cer + + // Move on if there are no certificates to install for this store type. + if (!certificates.Any()) + { + continue; + } + + WriteVerbose($"Importing certificates from '{folderPath}' into the {store} store."); + + // Import certificates into the appropriate store. + StoreName storeName = Extensions.GetStoreNameByFolderName(store); + foreach (var certificatePath in certificates) + { + WriteVerbose($"Importing certificate {certificatePath}"); + + // Load the certificate if it isn't already in the store. + X509Certificate2 certificate = new X509Certificate2(certificatePath, password); + string certName = certificate.GetNameInfo(X509NameType.SimpleName, false); + string thumbprint = certificate.Thumbprint; + certificate.Dispose(); + + // See if the cert is already on the local machine. + certificate = CertificateHelper.FindCertificateByThumbprint(thumbprint, storeName, StoreLocation.LocalMachine, "localhost"); + + // Only load the cert if it isn't on the local machine. + if (certificate == null) + { + CertificateHelper.LoadCertificateToStore(certificatePath, storeName, StoreLocation.LocalMachine, password); + certificate = CertificateHelper.FindCertificateByThumbprint(thumbprint, storeName, StoreLocation.LocalMachine, "localhost"); + } + + // Grant user rights, only if it's a pfx. + if (string.Equals(Path.GetExtension(certificatePath), ".pfx", StringComparison.OrdinalIgnoreCase)) + { + GrantRightsToCertificate(certificate, storeName, grantUsers); + } + } + } + } + + /// + /// Grants user access rights to the specified certificate. + /// + /// + /// + private void GrantRightsToCertificate(X509Certificate2 certificate, StoreName storeName, string[] users) + { + // Look for the unique name of the cert, so we can track down the file to set ACL's + string uniqueContainerName = null; + using (var rsa = certificate.GetRSAPrivateKey()) + { + RSACng rsaCng = (RSACng)rsa; + using (CngKey key = rsaCng.Key) + { + uniqueContainerName = key.UniqueName; + } + } + + // Gather properties from the cert. + string certName = certificate.GetNameInfo(X509NameType.SimpleName, false); + string thumbprint = certificate.Thumbprint; + + // Locate the private key in the registry. + var keyFilePath = CertificateHelper.FindKeyLocation(uniqueContainerName); + var pkFile = new FileInfo(Path.Combine(keyFilePath, uniqueContainerName)); + + // Grant user permissions to the certificate. + WriteVerbose($"Granting access to {certificate}:{thumbprint} to users {string.Join(",", users)}"); + foreach (var user in users) + { + Common.Cryptography.CertificateHelper.GrantRightsToPrivateKeys(pkFile, user); + } + } + + private List GetAllThumbprintsFromStore(StoreName name) + { + var certs = CertificateHelper.GetAllCertificates(name, StoreLocation.LocalMachine); + + return certs.ToList().Select(c => c.Thumbprint).ToList(); + } + + /// + /// Gets users to grant rights to on certificates. Returns IIS_IUSRS, microservice users, and nag/radium users. + /// + /// + /// + /// + /// + private string[] GetUsersToGrantRightsTo(string microserviceUser, string databaseUser, string gmsaPrefix) + { + // Read users that are running alkami services. + var services = ServiceController.GetServices(); + var alkamiServices = services.Where(service => service.ServiceName.ToLower().Contains("alkami")); + var users = alkamiServices.Select(service => GetServiceAccountUser(service)) + .Where(user => user != null); + + // Concatenate known users onto the end of the list of unqiue Alkami users. + users = users.Concat(new string[] { "IIS_IUSRS", microserviceUser, databaseUser }); + + // Determine nag/radium user by convention only if the gmsa prefix was passed in. + if (!string.IsNullOrWhiteSpace(gmsaPrefix)) + { + users = users.Concat(new string[] { $"fh\\{gmsaPrefix}.nag$", $"fh\\{gmsaPrefix}.radium$" }); + } + + // Only return the unique usernames. + users = users.Distinct(); + + // Remove any LocalSystem users. + users = users.Where(user => !string.Equals(user, "LocalSystem", StringComparison.OrdinalIgnoreCase)); + + return users.ToArray(); + } + + /// + /// Returns the user of the ServiceController service. + /// + /// + /// + private string GetServiceAccountUser(ServiceController service) + { + try + { + System.Management.SelectQuery sQuery = new System.Management.SelectQuery($"select startname from Win32_Service where name = '{service.ServiceName}'"); + using (System.Management.ManagementObjectSearcher mgmtSearcher = new System.Management.ManagementObjectSearcher(sQuery)) + { + foreach (System.Management.ManagementObject manageObject in mgmtSearcher.Get()) + { + string account = manageObject["Startname"].ToString(); + return account; + } + } + } + catch (Exception e) + { + WriteWarning($"Failed to determine user account for service {service.ServiceName} with error:\n{e.ToString()}"); + return null; + } + + return null; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesToSecretServer.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesToSecretServer.cs new file mode 100644 index 0000000..bd89751 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/ImportCertificatesToSecretServer.cs @@ -0,0 +1,38 @@ +using System; +using System.Management.Automation; + +namespace Alkami.Ops.Certificates +{ + [Cmdlet("Import", "CertificatesToSecretServer")] + [OutputType(typeof(string))] + public class ImportCertificatesToSecretServer : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string SecretUsername; + + [Parameter(Position = 1, Mandatory = true)] + public string SecretPassword; + + [Parameter(Position = 2, Mandatory = true)] + public string[] Servers; + + [Parameter(Position = 3, Mandatory = false)] + public string SecretSite = "https://alkami.secretservercloud.com"; + + [Parameter(Position = 4, Mandatory = false)] + public string FriendlySecretFolder = "ops.deployment-CertApi/FriendlyCertificates"; + + protected override void ProcessRecord() + { + var watch = new System.Diagnostics.Stopwatch(); + watch.Start(); + + using (var importer = new SecretServerImporter(SecretSite, SecretUsername, SecretPassword)) + { + importer.Import(Servers, FriendlySecretFolder); + } + + Console.WriteLine($"Finished Importing secrets to SecretServer in {watch.Elapsed}"); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/OptimizePodSecrets.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/OptimizePodSecrets.cs new file mode 100644 index 0000000..0f6e05a --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/OptimizePodSecrets.cs @@ -0,0 +1,51 @@ +using System; +using System.Management.Automation; + +namespace Alkami.Ops.Certificates +{ + /// + /// Downloads certificates for ALL environments under the FriendlyCertificates folder, combines them into a handful of secrets, and uploads them to the MachineSecrets folder. + /// This allows a server to download 4 secrets (web, app, common web, common app) instead of 200+ secrets. + /// + /// Username with which to authenticate + /// Password with which to authenticate + /// Site of Secret Server + /// Root folder for where all Friendly Certificates are stored. + /// Doesn't appear to actually be used? Just leave the defaults. + /// Root folder for where all Zipped Certificates will be placed. + [Cmdlet("Optimize", "PodSecrets")] + [OutputType(typeof(string))] + public class OptimizePodSecrets : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string SecretUsername; + + [Parameter(Position = 1, Mandatory = true)] + public string SecretPassword; + + [Parameter(Position = 2, Mandatory = false)] + public string SecretSite = "https://alkami.secretservercloud.com"; + + [Parameter(Position = 3, Mandatory = false)] + public string FriendlySecretFolder = "ops.deployment-CertApi/FriendlyCertificates"; + + [Parameter(Position = 4, Mandatory = false)] + public string[] ImportableUsers = new string[] { "CORP\\Site Reliability Engineers", "fh\\jumpbox.jenkins", "fh\\ci.migrate$" }; + + [Parameter(Position = 5, Mandatory = false)] + public string MachineSecretFolder = "ops.deployment-CertApi/MachineSecrets"; + + protected override void ProcessRecord() + { + var watch = new System.Diagnostics.Stopwatch(); + watch.Start(); + + using (var importer = new SecretServerImporter(SecretSite, SecretUsername, SecretPassword)) + { + importer.CreatePodSecrets(FriendlySecretFolder, MachineSecretFolder, ImportableUsers); + } + + Console.WriteLine($"Finished executing in {watch.Elapsed}"); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInFriendlySecretsFolder.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInFriendlySecretsFolder.cs new file mode 100644 index 0000000..713c1fa --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInFriendlySecretsFolder.cs @@ -0,0 +1,46 @@ +using System; +using System.Management.Automation; + +namespace Alkami.Ops.Certificates +{ + /// + /// Yeets certs for a given folder + /// + [Cmdlet("Remove", "SecretsInFriendlyCertificatesFolder")] + [OutputType(typeof(string))] + public class RemoveSecretsInFriendlyCertificatesFolder : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string SecretUsername; + + [Parameter(Position = 1, Mandatory = true)] + public string SecretPassword; + + [Parameter(Position = 2, Mandatory = false)] + public string SecretSite = "https://alkami.secretservercloud.com"; + + private string Folder = @"\ops.deployment-CertApi\FriendlyCertificates\"; + + protected override void ProcessRecord() + { + DeleteCertificatesInFolder(Folder); + } + + /// + /// Imports certificates to the machine from the MachineSecrets folder. + /// + /// + private void DeleteCertificatesInFolder(string Folder) + { + Console.WriteLine("Yeeting certificates from the secret server."); + + // Create secret server client. + using (var client = new SecretServer.SecretServerClient(SecretSite, SecretUsername, SecretPassword)) + { + // yeet zips + var secretFolder = client.GetFolder(Folder); + client.DeleteSecretsInFolder(secretFolder).GetAwaiter().GetResult(); + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInMachineSecretsFolder.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInMachineSecretsFolder.cs new file mode 100644 index 0000000..983fca0 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/Remove-SecretsInMachineSecretsFolder.cs @@ -0,0 +1,46 @@ +using System; +using System.Management.Automation; + +namespace Alkami.Ops.Certificates +{ + /// + /// Yeets certs for a given folder + /// + [Cmdlet("Remove", "SecretsInMachineSecretsFolder")] + [OutputType(typeof(string))] + public class RemoveSecretsInMachineSecretsFolder : Cmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string SecretUsername; + + [Parameter(Position = 1, Mandatory = true)] + public string SecretPassword; + + [Parameter(Position = 2, Mandatory = false)] + public string SecretSite = "https://alkami.secretservercloud.com"; + + private string Folder = @"\ops.deployment-CertApi\MachineSecrets\"; + + protected override void ProcessRecord() + { + DeleteCertificatesInFolder(Folder); + } + + /// + /// Imports certificates to the machine from the MachineSecrets folder. + /// + /// + private void DeleteCertificatesInFolder(string Folder) + { + Console.WriteLine("Yeeting certificates from the secret server."); + + // Create secret server client. + using (var client = new SecretServer.SecretServerClient(SecretSite, SecretUsername, SecretPassword)) + { + // yeet zips + var secretFolder = client.GetFolder(Folder); + client.DeleteSecretsInFolder(secretFolder).GetAwaiter().GetResult(); + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Cmdlets/WriteCertStoreHashToFile.cs b/Modules/Alkami.Ops.Certificates/Cmdlets/WriteCertStoreHashToFile.cs new file mode 100644 index 0000000..eb84250 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Cmdlets/WriteCertStoreHashToFile.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.Cmdlets +{ + [Cmdlet("Write", "CertStoreHashToFile")] + [OutputType(typeof(string))] + public class WriteCertStoreHashToFile : Cmdlet + { + [Parameter(Position = 0, Mandatory = false)] + public string FilePath { get; set; } = @"C:\Tools\CertificateManagement\TrackedThumbprints.json"; + + protected override void ProcessRecord() + { + WriteCertHashesToFile(); + } + + private void WriteCertHashesToFile() + { + List thumbprints = new List(); + string[] storesToSearch = new string[] { "my", "CertificateAuthority", "root", "trustedpeople" }; + foreach (string storeString in storesToSearch) + { + StoreName.TryParse(storeString, true, out StoreName storeName); + + X509Certificate2Collection Certificates = new X509Certificate2Collection(); + Certificates.AddRange(Common.Cryptography.CertificateHelper.GetAllCertificates(storeName, StoreLocation.LocalMachine)); + foreach (X509Certificate2 certificate in Certificates) + { + thumbprints.Add(certificate.Thumbprint); + } + } + + File.WriteAllText(FilePath, JsonConvert.SerializeObject(thumbprints)); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/CertificateInfo.cs b/Modules/Alkami.Ops.Certificates/Data/CertificateInfo.cs new file mode 100644 index 0000000..5cb7714 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/CertificateInfo.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Alkami.Ops.Certificates.Data +{ + internal class CertificateInfo + { + public readonly X509Certificate2 Certificate; + + public List Children = new List(); + public CertificateInfo Parent = null; + + public readonly string Name; + + public readonly string UniqueName; + + public readonly string FileName; + + public string Thumbprint => Certificate.Thumbprint; + + public List Stores = new List(); + + public readonly string Password; + + public Dictionary Servers = new Dictionary(); + public Dictionary Environments = new Dictionary(); + + public CertificateInfo(X509Certificate2 certificate, string filename, string password = null) + { + this.Certificate = certificate; + this.FileName = filename; + this.Password = password; + Name = certificate.GetNameInfo(X509NameType.SimpleName, false); + UniqueName = string.IsNullOrWhiteSpace(Name) ? (certificate.Thumbprint) : ($"{Name}-{certificate.Thumbprint}"); + UniqueName = UniqueName.Trim(); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/EnvironmentInfo.cs b/Modules/Alkami.Ops.Certificates/Data/EnvironmentInfo.cs new file mode 100644 index 0000000..5e8c94d --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/EnvironmentInfo.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Alkami.Ops.Certificates.Data +{ + internal class EnvironmentInfo + { + public EnvironmentInfo(string name, string type) + { + this.Name = name; + this.EnvironmentType = type; + } + + public readonly string Name; + public readonly string EnvironmentType; + + public List Servers = new List(); + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/SecretZipInfo.cs b/Modules/Alkami.Ops.Certificates/Data/SecretZipInfo.cs new file mode 100644 index 0000000..e24dd25 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/SecretZipInfo.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Alkami.Ops.Certificates.Data +{ + internal class SecretZipInfo + { + public List CertificateThumbprints { get; set; } + public string FileHash { get; set; } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/ServerInfo.cs b/Modules/Alkami.Ops.Certificates/Data/ServerInfo.cs new file mode 100644 index 0000000..5ed8a5f --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/ServerInfo.cs @@ -0,0 +1,48 @@ +using System; + +namespace Alkami.Ops.Certificates.Data +{ + internal class ServerInfo + { + public readonly string EnvironmentName = null; + public readonly string EnvironmentType = null; + public readonly string HostingProvider = null; + public readonly string ServerType = null; + public readonly string DatabaseUser = null; + public readonly string MicroUser = null; + + public readonly EnvironmentInfo Environment = null; + + public ServerInfo(ServerInfo existingServer, EnvironmentInfo environment) + { + this.EnvironmentName = existingServer.EnvironmentName; + this.EnvironmentType = existingServer.EnvironmentType; + this.HostingProvider = existingServer.HostingProvider; + this.ServerType = existingServer.ServerType; + this.MicroUser = existingServer.MicroUser; + this.DatabaseUser = existingServer.DatabaseUser; + this.Environment = environment; + } + + public ServerInfo(string environmentName, string environmentType, string hostingProvider, string serverType, string microUser, string databaseUser, EnvironmentInfo environment) + { + this.EnvironmentName = environmentName; + this.EnvironmentType = environmentType; + this.HostingProvider = hostingProvider; + this.ServerType = serverType; + this.Environment = environment; + this.MicroUser = microUser; + this.DatabaseUser = databaseUser; + } + + public bool IsApp() + { + return string.Equals(this.ServerType, "App", StringComparison.OrdinalIgnoreCase); + } + + public bool IsWeb() + { + return string.Equals(this.ServerType, "Web", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/StoreInfo.cs b/Modules/Alkami.Ops.Certificates/Data/StoreInfo.cs new file mode 100644 index 0000000..d751f7d --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/StoreInfo.cs @@ -0,0 +1,14 @@ +namespace Alkami.Ops.Certificates.Data +{ + internal class StoreInfo + { + public readonly string StoreName; + public readonly bool HasPrivateKey; + + public StoreInfo(string storeName, bool hasPrivateKey) + { + StoreName = storeName; + HasPrivateKey = hasPrivateKey; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Data/TrackedCertInfo.cs b/Modules/Alkami.Ops.Certificates/Data/TrackedCertInfo.cs new file mode 100644 index 0000000..e188edf --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Data/TrackedCertInfo.cs @@ -0,0 +1,12 @@ +namespace Alkami.Ops.Certificates.Data +{ + internal class TrackedCertInfo + { + public readonly string Thumbprint; + + public TrackedCertInfo(string thumbprint) + { + Thumbprint = thumbprint; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Properties/AssemblyInfo.cs b/Modules/Alkami.Ops.Certificates/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..eaf5fcb --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Alkami.Ops.Certificates")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Alkami.Ops.Certificates")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ad323736-de44-42d9-a574-f61a42c69cf8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Modules/Alkami.Ops.Certificates/Resources.Designer.cs b/Modules/Alkami.Ops.Certificates/Resources.Designer.cs new file mode 100644 index 0000000..894ca17 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Resources.Designer.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Alkami.Ops.Certificates { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Alkami.Ops.Certificates.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to param( + /// $serverString, + /// $exportPassword, + /// $importPath + ///) + /// + ///$servers = $serverString.Split(","); + /// + ///$exportCertificateZipPath = "C:/temp/certificateExport.zip"; + ///$script = { + /// $password = $using:exportPassword; + /// $exportPath = $using:exportCertificateZipPath; + /// + /// # Create the cert temp cert export directory for each server. + /// $tempPath = "C:/temp/certificateExport"; + /// if(Test-Path $tempPath) + /// { + /// Remove-Item -Path $tempPath -Recurse -Force; + /// } + /// New-Item -Path $tempPath -Ite [rest of string was truncated]";. + /// + internal static string ExportRemoteCertificates { + get { + return ResourceManager.GetString("ExportRemoteCertificates", resourceCulture); + } + } + } +} diff --git a/Modules/Alkami.Ops.Certificates/Resources.resx b/Modules/Alkami.Ops.Certificates/Resources.resx new file mode 100644 index 0000000..f5cc7e6 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Scripts\ExportRemoteCertificates.ps1;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Scripts/ExportRemoteCertificates.ps1 b/Modules/Alkami.Ops.Certificates/Scripts/ExportRemoteCertificates.ps1 new file mode 100644 index 0000000..2231be2 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Scripts/ExportRemoteCertificates.ps1 @@ -0,0 +1,85 @@ +param( + $serverString, + $exportPassword, + $importPath +) + +$servers = $serverString.Split(","); + +$exportCertificateZipPath = "C:/temp/certificateExport.zip"; +$script = { + $password = $using:exportPassword; + $exportPath = $using:exportCertificateZipPath; + + # Create the cert temp cert export directory for each server. + $tempPath = "C:/temp/certificateExport"; + if(Test-Path $tempPath) + { + Remove-Item -Path $tempPath -Recurse -Force; + } + New-Item -Path $tempPath -ItemType Directory; + + # Export all certificates and compress them. + try { + Write-Host "Exporting Certificates to $tempPath"; + Export-Certificates -exportPassword $password -exportPath $tempPath; + + $zipPath = "$tempPath/*"; + Write-Host "Zipping certificates at $zipPath to archive $exportPath"; + Compress-Archive -Path $zipPath -DestinationPath $exportPath -Force | Out-Null; + } + catch { + throw $_; + } + finally { + # Clean up exported certs. + if(Test-Path $tempPath) + { + Remove-Item -Path $tempPath -Recurse -Force; + } + } +} + +try +{ + # Export all of the certificates on each server. + Invoke-Command -ComputerName $servers -ScriptBlock $script; + + # Read all of the certificates back to the agent machine and unzip. + $copyToAgentScript = { + param($server) + + $certZipPath = Get-UncPath -filePath $using:exportCertificateZipPath -ComputerName $server; + + $serverImportDirectory = (Join-Path $using:importPath $server); + $serverImportFile = (Join-Path $serverImportDirectory "certs.zip"); + if(Test-Path $certZipPath) + { + if(!(Test-Path $serverImportDirectory)) + { + New-Item -Path $serverImportDirectory -ItemType Directory | Out-Null; + } + Write-Host "Copying $certZipPath to $serverImportFile"; + Move-Item -Path $certZipPath -Destination $serverImportFile -Force | Out-Null; + + Write-Host "Expanding archive $serverImportfile in $serverImportDirectory" + Expand-Archive -Path $serverImportFile -DestinationPath $serverImportDirectory -Force; + + Remove-Item -Path $serverImportFile -Force; + } + } + Invoke-Parallel -objects $servers -script $copyToAgentScript; +} +finally +{ + # Clean up the certificate export zip's on all of the servers if the process crashed. + foreach($server in $servers) + { + $certZipPath = Get-UncPath -filePath $exportCertificateZipPath -ComputerName $server; + if(Test-Path $certZipPath) + { + Write-Host "Cleaning up $certZipPath"; + Remove-Item -Path $certZipPath; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/Models/Folder.cs b/Modules/Alkami.Ops.Certificates/SecretServer/Models/Folder.cs new file mode 100644 index 0000000..8c61924 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/Models/Folder.cs @@ -0,0 +1,14 @@ +namespace Alkami.Ops.Certificates.SecretServer.Models +{ + internal class Folder + { + public int ID; + public string FolderName; + public string FolderPath; + public int ParentFolderID; + public int FolderTypeID; + public int SecretPolicyID; + public bool InheritSecretPolicy; + public bool InheritPermissions; + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/Models/Secret.cs b/Modules/Alkami.Ops.Certificates/SecretServer/Models/Secret.cs new file mode 100644 index 0000000..45bf3a6 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/Models/Secret.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Alkami.Ops.Certificates.SecretServer.Models +{ + internal class Secret + { + public int ID; + public string Name; + public int SecretTemplateID; + public int FolderID = -1; + public bool Active; + public List Items; + public int? LauncherConnectAsSecretID; + public int? CheckOutMinutesRemaining; + public bool? CheckedOut; + public string CheckOutUserDisplayName; + public int? CheckOutUserID; + public bool IsRestricted; + public bool IsOutOfSync; + public string OutOfSyncReason; + public bool? AutoChangeEnabled; + public bool? AutoChangeNextPassword; + public bool? RequiresApprovalForAccess; + public bool? RequiresComment; + public bool? CheckOutEnabled; + public int? CheckOutIntervalMinutes; + public bool? CheckOutChangePasswordEnabled; + public int? AccessRequestWorkflowMapID; + public bool? ProxyEnabled; + public bool? SessionRecordingEnabled; + public bool? RestrictSshCommands; + public bool? AllowOwnersUnrestrictedSshCommands; + public bool? IsDoubleLock; + public int? DoubleLockId; + public bool? EnableInheritPermissions; + public int? PasswordTypeWebScriptID; + public int? SiteID; + public bool? EnableInheritSecretPolicy; + public int? SecretPolicyID; + public string LastHeartBeatStatus; + public DateTime? LastHeartBeatCheck; + public int? FailedPasswordChangeAttempts; + public DateTime? LastPasswordChangeAttempt; + public string SecretTemplateName; + public string LastAccessed; + + /// + /// Sets string field values that are not files. + /// + /// The secret field to set. + /// + public string this[string field] + { + get + { + if (Items != null) + { + return Items.Where(f => !f.IsFile && string.Equals(f.FieldName.Trim(), field, StringComparison.OrdinalIgnoreCase)).First().ItemValue; + } + else + { return null; } + } + set + { + if (Items == null) + { + Items = new List(); + } + + Items.Where(f => !f.IsFile && string.Equals(f.FieldName.Trim(), field, StringComparison.OrdinalIgnoreCase)).First().ItemValue = value; + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretField.cs b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretField.cs new file mode 100644 index 0000000..6d721d2 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretField.cs @@ -0,0 +1,15 @@ +namespace Alkami.Ops.Certificates.SecretServer.Models +{ + internal class SecretField + { + public int ItemID; + public string Filename; + public string ItemValue; + public int FieldID; + public string FieldName; + public string FieldDescription; + public bool IsFile; + public bool IsNotes; + public bool IsPassword; + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretSearch.cs b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretSearch.cs new file mode 100644 index 0000000..09a5a1d --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretSearch.cs @@ -0,0 +1,10 @@ +namespace Alkami.Ops.Certificates.SecretServer.Models +{ + internal class SecretSearch + { + public int ID; + public string Value; + + public string Name => Value.Split('-')[2].Trim(); + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretTemplate.cs b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretTemplate.cs new file mode 100644 index 0000000..e5fa4be --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/Models/SecretTemplate.cs @@ -0,0 +1,8 @@ +namespace Alkami.Ops.Certificates.SecretServer.Models +{ + internal class SecretTemplate + { + public int ID; + public string Name; + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServer/SecretServerClient.cs b/Modules/Alkami.Ops.Certificates/SecretServer/SecretServerClient.cs new file mode 100644 index 0000000..6da8e57 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServer/SecretServerClient.cs @@ -0,0 +1,701 @@ +using Alkami.Ops.Certificates.SecretServer.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Alkami.Ops.Certificates.SecretServer +{ + internal class SecretServerClient : IDisposable + { + public readonly string Site; + private readonly string _username; + private readonly string _password; + private readonly string _apiEndpoint; + private string _accessToken; + + private HttpClient _httpClient; + + public SecretServerClient(string site, string username, string password) + { + this.Site = site; + this._username = username; + this._password = password; + + _apiEndpoint = $"{Site}/api/v1"; + + // Create httpclient. + System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; + _httpClient = new HttpClient(); + + // Obtain auth token. + Authenticate().Wait(); + } + + /// + /// Authenticates and grabs an oauth token, which is attached to common headers for future usage of the secret server client. + /// + /// + private async Task Authenticate() + { + var data = new + { + username = _username, + password = _password, + grant_type = "password" + }; + var test = $"username={_username}&password={_password}&grant_type=password"; + + var body = JsonConvert.SerializeObject(data); + var content = new StringContent(test); + + var tokenRoute = $"{Site}/oauth2/token"; + var response = await _httpClient.PostAsync(tokenRoute, content); + var jsonResponse = await response.Content.ReadAsStringAsync(); + + JObject parsedResponse = JObject.Parse(jsonResponse); + _accessToken = parsedResponse["access_token"].ToString(); + + // Add auth token to the default headers of the http client. + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_accessToken}"); + } + + /// + /// Returns a single folder with the name. + /// Throws an exception if there is more than one server with a particular folder name. + /// + /// The name of the folder to search for. + /// The parent folder ID, to look for names within a particular folder. + /// The Folder + public Folder GetFolderByName(string folderName, int parentID = -1) + { + var folders = GetFoldersByName(folderName, parentID); + + // Limit to exact name. Searching for POD1 will also return POD10 + folders = folders.Where(folder => string.Equals(folder.FolderName, folderName, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (folders == null || folders.Length == 0) + { + return null; + } + else if (folders.Length > 1) + { + throw new Exception($"Found more than one folder with name {folderName}. Investigate."); + } + + return folders.First(); + } + + /// + /// Returns folders with a given name. + /// Only returns the first page of results (10 entries), because their documentation doesn't describe how pagination works! + /// + /// The name of the folder to search for + /// The Folder + public Folder[] GetFoldersByName(string folderName, int parentID = -1) + { + var parameters = $"?filter.includeRestricted=true&filter.searchText={folderName}"; + if (parentID >= 0) + { + parameters += $"&filter.parentFolderId={parentID}"; + } + + var url = $"{_apiEndpoint}/folders{parameters}"; + var folders = GetPageEnumerable(url); + return folders.ToArray(); + } + + /// + /// Returns the folders underneath a parent folder. + /// + /// The folder to pull folders from. + /// + public Folder[] GetFoldersByParentFolder(int parentID) + { + if (parentID <= 0) + { + return null; + } + + var parameters = $"?filter.parentFolderId={parentID}"; + var url = $"{_apiEndpoint}/folders/{parameters}"; + var folders = GetPageEnumerable(url); + return folders.ToArray(); + } + + /// + /// Creates a secret server folder given a parent folder. + /// + /// The parent folder of the folder to create. + /// The name of the folder to create. + /// True if the folder should inherit the permissions of the parent folder. + /// True if the folder should inherit the secret policies of the parent folder. + /// The Folder + public async Task CreateFolderAsync(Folder parentFolder, string folderName, bool inheritPermissions = true, bool inheritSecretPolicy = true) + { + if (parentFolder == null) + { + return null; + } + + return await CreateFolderAsync(parentFolder.ID, folderName, inheritPermissions, inheritSecretPolicy); + } + + /// + /// Creates a secret server folder given a parent folder. + /// + /// The parent folder ID of the folder to create. + /// The name of the folder to create. + /// True if the folder should inherit the permissions of the parent folder. + /// True if the folder should inherit the secret policies of the parent folder. + /// The Folder + public async Task CreateFolderAsync(int parentFolderId, string folderName, bool inheritPermissions = true, bool inheritSecretPolicy = true) + { + var response = await _httpClient.GetAsync($"{_apiEndpoint}/folders/stub"); + var jsonResponse = await response.Content.ReadAsStringAsync(); + + JObject parsedResponse = JObject.Parse(jsonResponse); + + parsedResponse["folderName"] = folderName; + parsedResponse["folderTypeId"] = 1; + parsedResponse["inheritPermissions"] = inheritPermissions; + parsedResponse["inheritSecretPolicy"] = inheritSecretPolicy; + parsedResponse["parentFolderId"] = parentFolderId; + + var createFolderArgsJson = parsedResponse.ToString(); + var createFolderContentBytes = Encoding.UTF8.GetBytes(createFolderArgsJson); + var byteContent = new ByteArrayContent(createFolderContentBytes); + byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var createResponse = await _httpClient.PostAsync($"{_apiEndpoint}/folders", byteContent); + var createResponseJson = await response.Content.ReadAsStringAsync(); + + return GetFolderByName(folderName, parentFolderId); + } + + /// + /// Deletes all secrets inside the given folder. Be careful with this, really! + /// + /// The folder to remove secrets in. + /// + public async Task DeleteSecretsInFolder(Folder folder) + { + var secrets = GetSecretsByFolder(folder, true); + foreach (var secret in secrets) + { + await DeleteSecret(secret.ID); + } + } + + /// + /// Gets a folder from the secret server with a given folder path. Returns null if the folder does not exist. + /// + /// + /// + public Folder GetFolder(string folderPath) + { + // Swap slashes just in case and parse apart the folder path. + folderPath = folderPath.Replace("\\", "/"); + var folderSplit = folderPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + // Find the parent folder. + string parentFolderName = folderSplit[0]; + Folder parentFolder = GetFolderByName(parentFolderName); + if (parentFolder == null) + { + throw new Exception($"Could not find parent folder named {parentFolderName}"); + } + + // Find, or create, the other folders up through the path. + foreach (var subfolder in folderSplit.Skip(1)) + { + var folder = GetFolderByName(subfolder, parentFolder.ID); + + if (folder == null) + { + return null; + } + + parentFolder = folder; + } + + return parentFolder; + } + + /// + /// Get a folder from the secret server with a given folder path, or creates the folders if they do not exist. + /// + /// + /// + public async Task GetOrAddFolderAsync(string folderPath) + { + // Swap slashes just in case and parse apart the folder path. + folderPath = folderPath.Replace("\\", "/"); + var folderSplit = folderPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + // Find the parent folder. + string parentFolderName = folderSplit[0]; + Folder parentFolder = GetFolderByName(parentFolderName); + if (parentFolder == null) + { + throw new Exception($"Could not find parent folder named {parentFolderName}"); + } + + // Find, or create, the other folders up through the path. + foreach (var subfolder in folderSplit.Skip(1)) + { + var folder = GetFolderByName(subfolder, parentFolder.ID); + + if (folder == null) + { + folder = await CreateFolderAsync(parentFolder, subfolder); + } + + parentFolder = folder; + } + + return parentFolder; + } + + /// + /// Returns a specific secret template type by name. + /// Throws an exception if the template name matches more than one template type. + /// + /// Name of the template to search for. + /// The template with the given name. + public SecretTemplate GetSecretTemplateByName(string templateName) + { + var templates = GetSecretTemplatesByName(templateName); + + // Limit to exact name. The secret API search is open-ended wildcard style. + templates = templates.Where(template => string.Equals(template.Name, template.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (templates == null || templates.Length == 0) + { + return null; + } + else if (templates.Length > 1) + { + throw new Exception($"There was more than one template type with the name {templateName}"); + } + else + { + return templates.First(); + } + } + + /// + /// Returns secret template types given a name. + /// + /// Name of the template. + /// + public SecretTemplate[] GetSecretTemplatesByName(string templateName) + { + var searchString = $"?filter.searchText={templateName}"; + var url = $"{_apiEndpoint}/secret-templates{searchString}"; + var secretTemplates = GetPageEnumerable(url); + return secretTemplates.ToArray(); + } + + /// + /// Gets a list of all secrets from a given folder. + /// Secrets returned only include top-level information, and does not include fields or extended properties. + /// Use GetSecretByID() to grab all of the information for a particular secret. + /// + /// Folder to pull secrets from + /// True if you want the result to include secrets from subfolders. + /// + public Secret[] GetSecretsByFolder(Folder folder, bool includeSubfolders = false) + { + var filter = $"?filter.folderId={folder.ID}"; + if (includeSubfolders) + { + filter += "&filter.includeSubFolders=True"; + } + var url = $"{_apiEndpoint}/secrets/{filter}"; + + var secrets = GetPageEnumerable(url); + return secrets.ToArray(); + } + + /// + /// Returns a Secret Server style paging enumerable that will continue to load pages as the IEnumerable is enumerated. + /// + /// The type of the object to deserialize from the request. + /// The query URL + /// + private IEnumerable GetPageEnumerable(string url) + { + bool hasParameters = url.IndexOf('?') >= 0; + int pageCounter = 0; + int numPages = 0; + int entriesPerPage = 10; + do + { + // Build the skip/take params depending on the page. + int skip = entriesPerPage * pageCounter; + string paramString = hasParameters ? "&" : "?"; + paramString += $"Skip={skip}&Take={entriesPerPage}"; + string endpoint = url + paramString; + + // Unfortunately async enumerables won't exist until C# 8. + // The sync enumerable is preferable to duplicating this code everywhere. + var response = _httpClient.GetAsync(endpoint, HttpCompletionOption.ResponseContentRead).GetAwaiter().GetResult(); + var jsonResponse = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + JObject parsedResponse = JObject.Parse(jsonResponse); + int total = int.Parse(parsedResponse["total"].ToString()); + if (total <= 0) + { + yield break; + } + + // Figure out how many pages, and how many entries per page there are. + entriesPerPage = int.Parse(parsedResponse["take"].ToString()); + numPages = int.Parse(parsedResponse["pageCount"].ToString()); + int currentPage = int.Parse(parsedResponse["currentPage"].ToString()); + + var records = parsedResponse["records"]; + var count = records.Count(); + for (int i = 0; i < count; i++) + { + yield return records[i].ToObject(); + } + + ++pageCounter; + } while (pageCounter < numPages); + } + + /// + /// Returns a secret (with full data) with the given ID. + /// + /// ID of the secret. + /// + public async Task GetSecretByID(int ID) + { + var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{ID}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(responseContent); + return result; + } + + /// + /// Looks up a single secret by name. + /// Throws an exception if there is more than one secret that matches the name. + /// + /// Name of the secret + /// Optional parent folder ID + /// + public SecretSearch FindSecretByName(string name, int folderID = -1) + { + var secrets = FindSecretsByName(name, folderID) + ?.Where(secret => string.Equals(secret?.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase)); + int count = secrets.Count(); + if (secrets == null || count == 0) + { + return null; + } + else if (count > 1) + { + throw new Exception($"Found more than one secret with name {name}"); + } + + return secrets.First(); + } + + /// + /// Looks up secrets by name. + /// + /// Name of the secret + /// Optional parent folder ID + /// + public SecretSearch[] FindSecretsByName(string name, int folderID = -1) + { + var filters = $"?filter.includeRestricted=true&filter.searchtext={name}"; + if (folderID > 0) + { + filters += $"&filter.folderId={folderID}"; + } + var url = $"{_apiEndpoint}/secrets/lookup{filters}"; + var secrets = GetPageEnumerable(url); + return secrets.ToArray(); + } + + /// + /// Returns a shimmed out default-value secret modeled after the provided templatID. + /// + /// Template ID to model the secret after + /// Folder to place the secret in. + /// + public async Task CreateSecretStubByTemplateIDAsync(int templateId, string secretName, int folderId) + { + var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/stub?filter.secrettemplateid={templateId}&filter.folderId={folderId}"); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(responseContent); + result.Name = secretName; + result.SiteID = 1; + return result; + } + + /// + /// Creates a secret, given a secret object. + /// It is highly suggested that you create secrets with CreateSecretStubByTemplateID in order to construct a valid Secret object based on a template type. + /// + /// The secret to create. + /// + public async Task CreateSecret(Secret secret) + { + // Do some basic validation to make sure this isn't going to do something undefined. + if (string.IsNullOrWhiteSpace(secret.Name)) + { + throw new Exception("Must define a secret name when creating new secrets."); + } + else if (secret.FolderID <= 0) + { + throw new Exception("Must specify a parent folder to place new secrets in."); + } + + var json = JsonConvert.SerializeObject(secret); + var createFolderContentBytes = Encoding.UTF8.GetBytes(json); + var byteContent = new ByteArrayContent(createFolderContentBytes); + byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var createResponse = await _httpClient.PostAsync($"{_apiEndpoint}/secrets/", byteContent); + var responseContent = await createResponse.Content.ReadAsStringAsync(); + if (!createResponse.IsSuccessStatusCode) + { + throw new Exception(responseContent); + } + + var result = JsonConvert.DeserializeObject(responseContent); + return result; + } + + /// + /// Updates a secret. + /// + /// + /// + public async Task UpdateSecret(Secret secret) + { + // Do some basic validation to make sure this isn't going to do something undefined. + if (string.IsNullOrWhiteSpace(secret.Name)) + { + throw new Exception("Must specify a valid secret name when updating a secret."); + } + else if (secret.FolderID <= 0) + { + throw new Exception("Secret must have a valid parent folder ID."); + } + else if (secret.ID <= 0) + { + throw new Exception("Can only update secrets with a valid secret ID"); + } + + var json = JsonConvert.SerializeObject(secret); + var createFolderContentBytes = Encoding.UTF8.GetBytes(json); + var byteContent = new ByteArrayContent(createFolderContentBytes); + byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var createResponse = await _httpClient.PutAsync($"{_apiEndpoint}/secrets/{secret.ID}", byteContent); + var responseContent = await createResponse.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(responseContent); + return result; + } + + /// + /// Uploads a file to a secret with the given field name. + /// + /// The secret to upload the file to. + /// The field name of the file on the secret + /// The name of the file. + /// The byte data of the file. + /// + public async Task UploadFile(Secret secret, string fieldName, string filename, byte[] bytes) + { + var data = new + { + fileName = filename, + fileAttachment = bytes + }; + + // Secret Server doesn't want the field ID, or a url encoded field name where spaces turn to %20's, + // It arbitrarily wants field name spaces replaced with dashes. + // Cheers to three hours I won't get back. + fieldName = fieldName.Replace(" ", "-"); + + var json = JsonConvert.SerializeObject(data); + var createFolderContentBytes = Encoding.UTF8.GetBytes(json); + var byteContent = new ByteArrayContent(createFolderContentBytes); + byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var createResponse = await _httpClient.PutAsync($"{_apiEndpoint}/secrets/{secret.ID}/fields/{fieldName}", byteContent); + return createResponse.IsSuccessStatusCode; + } + + /// + /// Uploads a file to a secret. + /// Throws an exception if there is more than one field type on the secret that is a file. + /// + /// The secret to upload the file to. + /// The bytes of the file. + /// + public async Task UploadFile(Secret secret, string filename, byte[] bytes) + { + var fileFields = secret.Items.Where(item => item.IsFile); + if (fileFields.Count() > 1) + { + throw new Exception("There is more than one field on this secret that is a file. Please specify the field name."); + } + var field = fileFields.First(); + + return await UploadFile(secret, field.FieldName, filename, bytes); + } + + /// + /// Uploads a file to a secret given a file path. + /// Throws an exception if there is more than one field type on the secret that is a file. + /// + /// The secret to upload the file to. + /// The bytes of the file. + /// + public async Task UploadFile(Secret secret, string filepath) + { + //Secret secret, string filename, byte[] bytes + if (!File.Exists(filepath)) + { + throw new FileNotFoundException($"Could not locate file {filepath}"); + } + + var filename = Path.GetFileName(filepath); + var bytes = File.ReadAllBytes(filepath); + return await UploadFile(secret, filename, bytes); + } + + /// + /// Downloads a file from a secret with the given field name. + /// + /// The secret to upload the file to. + /// The field name of the file on the secret + /// The name of the file. + /// The byte data of the file. + /// + public async Task DownloadFile(Secret secret, string fieldName) + { + fieldName = fieldName.Replace(" ", "-"); + + var response = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{secret.ID}/fields/{fieldName}"); + if (!response.IsSuccessStatusCode) + { + return null; + } + var responseContent = await response.Content.ReadAsByteArrayAsync(); + return responseContent; + } + + /// + /// Downloads a file from a secret. + /// Throws an exception if there is more than one field type on the secret that is a file. + /// + /// The secret to upload the file to. + /// The bytes of the file. + /// + public async Task DownloadFile(Secret secret) + { + var fileFields = secret?.Items.Where(item => item.IsFile); + if (fileFields == null) + { + return null; + } + else if (fileFields.Count() > 1) + { + throw new Exception("There is more than one field on this secret that is a file. Please specify the field name."); + } + var field = fileFields.First(); + + return await DownloadFile(secret, field.FieldName); + } + + /// + /// Downloads a file from a Secret to a given file path. + /// Throws an exception if there is more than one field type on the secret that is a file. + /// + /// The local file location where the file will be stored. + /// The secret to download the file from. + /// true if a file was successfully downloaded. + public async Task DownloadFile(string filepath, Secret secret) + { + var bytes = await DownloadFile(secret); + if (bytes == null) + { + return false; + } + + File.WriteAllBytes(filepath, bytes); + return true; + } + + /// + /// Deletes a secret. + /// + /// + /// + public async Task DeleteSecret(Secret secret) + { + return await DeleteSecret(secret.ID); + } + + /// + /// Deletes a secret. + /// + /// + /// + public async Task DeleteSecret(int secretId) + { + var response = await _httpClient.DeleteAsync($"{_apiEndpoint}/secrets/{secretId}"); + return response.IsSuccessStatusCode; + } + + /// + /// Compares the hash provided to the hash on the specified secret in Secret Server + /// + /// Secret to compare against + /// Local hash to use for comparison + /// Returns true if the hashes *DO NOT* match as this indicates a changed secret. + public async Task DetectChanges(int secretId, string md5Hash) + { + var remoteHashResult = await _httpClient.GetAsync($"{_apiEndpoint}/secrets/{secretId}/fields/file-hash"); + + if (remoteHashResult.IsSuccessStatusCode) + { + var remoteHash = remoteHashResult.Content.ReadAsStringAsync().Result; + // Hashes come back wrapped in quotes. Rip them out. + remoteHash = remoteHash.Trim('"'); + var comparisonResult = string.Compare(md5Hash, remoteHash, true); + + if (0 == comparisonResult) + { + return false; + } + else + { + return true; + } + } + else + { + throw new HttpRequestException("Exception getting file hash from Secret Server: " + remoteHashResult.RequestMessage); + } + } + + /// + /// Disposes the secret server client. + /// + public void Dispose() + { + _httpClient?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/SecretServerImporter.cs b/Modules/Alkami.Ops.Certificates/SecretServerImporter.cs new file mode 100644 index 0000000..d54e56b --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/SecretServerImporter.cs @@ -0,0 +1,787 @@ +using Alkami.Ops.Certificates.Data; +using Alkami.Ops.Certificates.SecretServer; +using Alkami.Ops.Certificates.Utilities; +using Alkami.Ops.Common.Cryptography; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management.Automation; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Alkami.Ops.Certificates +{ + internal class SecretServerImporter : IDisposable + { + private readonly string _tempPath; + private readonly string _globalPassword; + + private readonly SecretServerClient _secretServerClient; + + private const string CertificateTemplateTypeName = "Automated_Certificate"; + private readonly string ExportCertificatesScript; + private static long certCount = 0; + + /// + /// Constructor. + /// + /// The base URL of the secret server site. + /// Secret Server username with API access. + /// Secret server user password. + public SecretServerImporter(string secretSite, string secretUser, string secretPassword) + { + _tempPath = Path.Combine(Path.GetTempPath(), "CertificateExport"); + if (Directory.Exists(_tempPath)) + { + Extensions.ClearDirectory(_tempPath); + } + else + { + Directory.CreateDirectory(_tempPath); + } + + _globalPassword = GeneratePassword(); + + _secretServerClient = new SecretServerClient(secretSite, secretUser, secretPassword); + + // Read in powershell scripts. + ExportCertificatesScript = Resources.ExportRemoteCertificates; + } + + /// + /// Cleanup + /// + public void Dispose() + { + if (Directory.Exists(_tempPath)) + { + Directory.Delete(_tempPath, true); + } + + _secretServerClient?.Dispose(); + } + + /// + /// Creates machine-friendly per-pod secrets, so a server can download 4 secrets instead of 200. + /// + /// + /// + public void CreatePodSecrets(string friendlySecretBaseFolderPath, string machineSecretBaseFolderPath, string[] importableUsers) + { + var baseFolder = _secretServerClient.GetOrAddFolderAsync(friendlySecretBaseFolderPath).GetAwaiter().GetResult(); + + // The secret server will only return -all- folders under a subfolder. Can't just query for folders at a time. + var folders = _secretServerClient.GetFoldersByParentFolder(baseFolder.ID); + + // Parse apart the paths of the folders to strip off the base path. + friendlySecretBaseFolderPath = friendlySecretBaseFolderPath.Replace('/', '\\'); + var folderPaths = folders.Select(folder => folder.FolderPath) + .Select(path => path.Substring(path.IndexOf(friendlySecretBaseFolderPath) + friendlySecretBaseFolderPath.Length)); + + // Parse apart the environment types, and pods that exist (+ common) + // Levels: + // 1) EnvironmentType + // 2) Environment (or Common) + // 3) Web / App / All + var splitPaths = folderPaths.Select(path => path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + + var environmentTypes = splitPaths + .Where(split => split.Length >= 1) + .Select(split => split[0]) + .Distinct(); + + foreach (var environmentType in environmentTypes) + { + var environments = splitPaths + .Where(split => split.Length >= 2) + .Where(split => string.Equals(split[0], environmentType, StringComparison.OrdinalIgnoreCase)) + .Select(split => split[1]) + .Distinct(); + + foreach (var environment in environments) + { + CreatePodSecrets(friendlySecretBaseFolderPath, machineSecretBaseFolderPath, environmentType, environment, importableUsers); + } + } + } + + /// + /// Creates machine-friendly per-pod secrets, so a server can download 4 secrets instead of 200. + /// + /// Secret server path of the friendly secrets folder. + /// Secret server path of the machine per-pod secrets folder. + /// The type of environment. + /// The name of the environment. (Or "Common") + private void CreatePodSecrets(string friendlySecretBaseFolderPath, string machineSecretBaseFolderPath, string environmentType, string environment, string[] importableUsers) + { + using (PowerShell powerShellSession = PowerShell.Create()) + { + var environmentPath = Path.Combine(friendlySecretBaseFolderPath, environmentType, environment); + string[] serverTypes = new string[] { "All", "Web", "App" }; + + // Get the secret template type. + var secretTemplateType = _secretServerClient.GetSecretTemplateByName(CertificateTemplateTypeName); + + foreach (var serverType in serverTypes) + { + // Clear out the temp path directory. Only one of these processes should run at a time. + // TODO: Rewrite the function to be pathed by environment type and environment so multiple of them can be run in parallel. + if (Directory.Exists(_tempPath) && Directory.EnumerateFiles(_tempPath).Any()) + { + Extensions.ClearDirectory(_tempPath); + } + + var secretsPath = Path.Combine(environmentPath, serverType); + + var folder = _secretServerClient.GetOrAddFolderAsync(secretsPath).GetAwaiter().GetResult(); + var secrets = _secretServerClient.GetSecretsByFolder(folder); + + // Load the full secrets. + var fullSecrets = secrets.Select(secret => _secretServerClient.GetSecretByID(secret.ID).GetAwaiter().GetResult()); + + // Download each secret to a separate folder by ID. + foreach (var secret in fullSecrets) + { + // Download the zip. + Console.WriteLine($"Attempting to download {secret.ID}"); + var zipName = $"{secret.ID}.zip"; + var zipDownloadLocation = Path.Combine(_tempPath, zipName); + if (!_secretServerClient.DownloadFile(zipDownloadLocation, secret).GetAwaiter().GetResult()) + { + Console.WriteLine($"Failed to download secret ID {secret.ID}"); + continue; + } + + // Unzip the file to a per-secret folder. + var outputFolder = Path.Combine(_tempPath, secret.ID.ToString()); + if (!Directory.Exists(outputFolder)) + { + Directory.CreateDirectory(outputFolder); + } + + // Unzip it. + ZipFile.ExtractToDirectory(zipDownloadLocation, outputFolder); + + var pfxFiles = Directory.GetFiles(outputFolder, "*.pfx", SearchOption.AllDirectories); + foreach (var file in pfxFiles) + { + // re-export secret password with global password + var cert = new X509Certificate2(file, secret["Import Password"], X509KeyStorageFlags.Exportable); + byte[] exportedBytes = cert.Export(X509ContentType.Pkcs12, _globalPassword); + File.WriteAllBytes(file, exportedBytes); + } + } + + // Copy all of the certificates together, preserving folder paths. + var podSecretOutputFolder = Path.Combine(_tempPath, "PodSecretOutputFolder"); + if (!Directory.Exists(podSecretOutputFolder)) + { + Directory.CreateDirectory(podSecretOutputFolder); + } + var secretFolders = Directory.GetDirectories(_tempPath); + foreach (var sourceFolder in secretFolders) + { + // Source directories, a folder for each secret server secret. + var sourceDirectories = Directory.GetDirectories(sourceFolder, "*", SearchOption.AllDirectories); + + // Create folders in the destination folder + foreach (string dirPath in sourceDirectories) + { + var dstPath = dirPath.Replace(sourceFolder, podSecretOutputFolder); + if (!Directory.Exists(dstPath)) + { + Directory.CreateDirectory(dstPath); + } + } + + // Copy over the secrets. + foreach (string newPath in Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories)) + { + // If the file already exists in the combined secret, and it isn't the same file, come up with a new non-duplicate name and copy the secret over. + var podSecretDestinationPath = newPath.Replace(sourceFolder, podSecretOutputFolder); + + int counter = 1; + bool doCopy = true; + do + { + // Copy over the file if it doesn't exist. + if (!File.Exists(podSecretDestinationPath)) + { + break; + } + + // If the files are equal, the cert already exists in the combined folder, so break out. + if (Extensions.FilesAreEqual(new FileInfo(newPath), new FileInfo(podSecretDestinationPath))) + { + doCopy = false; + break; + } + + // Otherwise we're going to try to come up with a unique filename. + // This loop is going to keep going until we either find the exact same cert, or we arrive on a unique file name. + string nonConflictingPath = Path.Combine( + Path.GetDirectoryName(newPath), + Path.GetFileNameWithoutExtension(newPath), + counter.ToString(), + Path.GetExtension(newPath)); + podSecretDestinationPath = nonConflictingPath; + counter++; + } while (true); + + if (doCopy) + { + File.Copy(newPath, podSecretDestinationPath, true); + } + } + } + + // Zip up the combined certs. + var podSecretName = serverType; + var podSecretZipPath = Path.Combine(_tempPath, $"{podSecretName}.zip"); + ZipFile.CreateFromDirectory(podSecretOutputFolder, podSecretZipPath); + + // Create secret with combined zip. + var secretPath = Path.Combine(machineSecretBaseFolderPath, environmentType, environment); + var destinationSecretFolder = _secretServerClient.GetOrAddFolderAsync(secretPath).GetAwaiter().GetResult(); + + // See if a secret already exists, and if it does, delete it. + var existingSecret = _secretServerClient.FindSecretByName(serverType, destinationSecretFolder.ID); + if (existingSecret != null) + { + _secretServerClient.DeleteSecret(existingSecret.ID).GetAwaiter().GetResult(); + } + + // Create the new secret stub by template ID. + var secretStub = _secretServerClient.CreateSecretStubByTemplateIDAsync(secretTemplateType.ID, podSecretName + "-" + destinationSecretFolder.ID, destinationSecretFolder.ID).GetAwaiter().GetResult(); + + var currentZipHash = ""; + using (var stream = File.OpenRead(podSecretZipPath)) + { + currentZipHash = Extensions.GetMd5HashString(stream); + } + + // Fill out secret details. + secretStub["Import Password"] = _globalPassword; + secretStub["Certificate Name"] = podSecretName; + secretStub["Thumbprint"] = "N/A"; + secretStub["ExpirationDate"] = ""; + secretStub["File Hash"] = currentZipHash; + + // Create the secret. + var newSecret = _secretServerClient.CreateSecret(secretStub).GetAwaiter().GetResult(); + + // Upload the zip. + _secretServerClient.UploadFile(newSecret, podSecretZipPath).GetAwaiter().GetResult(); + } + } + } + + /// + /// Imports certificates from all of the given servers into the secret server. + /// + /// List of server hostnames. + /// Secret server folder path to drop the secrets in. + public void Import(IEnumerable servers, string baseSecretFolder) + { + // Make sure the servers are unique just in case. + servers = servers.Select(s => s.ToLower()).Distinct(); + + var serverData = new Dictionary(); + var environmentData = new Dictionary(); + var certificateData = new Dictionary(); + + if (Directory.Exists(_tempPath)) + { + // Remove directories and files under _tempPath. + foreach (var directory in Directory.GetDirectories(_tempPath)) + { + Directory.Delete(directory, true); + } + + foreach (var file in Directory.GetFiles(_tempPath)) + { + File.Delete(file); + } + } + + // Gather server/environment data from the servers. + using (var timer = new QuickWatch("GatherEnvironmentData")) + { + GatherEnvironmentData(servers, ref environmentData, ref serverData); + } + + // Download certificates from each server. + using (var timer = new QuickWatch("DownloadCertificatesFromServers")) + { + DownloadCertificatesFromServers(servers, _tempPath); + } + + // Build the tree of certificate information, to weed out unique certificates that need uploading. + using (var timer = new QuickWatch("BuildCertificateData")) + { + BuildCertificateData(_tempPath, ref serverData, ref certificateData); + } + + // Upload the secrets to the secret server. + using (var timer = new QuickWatch("UploadCertificatesToSecretServer")) + { + UploadCertificatesToSecretServer(baseSecretFolder, ref environmentData, ref certificateData); + } + } + + /// + /// Gathers environment data from each of the servers, and tracks the data in _environmentData and _serverData + /// + /// + private void GatherEnvironmentData(IEnumerable servers, ref Dictionary environmentData, ref Dictionary serverData) + { + // Read server/environment data from all of the servers. + foreach (var server in servers) + { + // Grab the server name/type/envType/host from the server's machine config. + var serverInfo = Extensions.GetServerInfo(server); + + // Look up the environment. + var environmentNameLower = serverInfo.EnvironmentName.ToLower(); + EnvironmentInfo environment = null; + if (!environmentData.TryGetValue(environmentNameLower, out environment)) + { + environment = new EnvironmentInfo(serverInfo.EnvironmentName, serverInfo.EnvironmentType); + environmentData[environmentNameLower] = environment; + } + + // Recreate the server to include the environment. + serverInfo = new ServerInfo(serverInfo, environment); + serverData[server] = serverInfo; + + // Record the server in the environment. + environment.Servers.Add(serverInfo); + } + } + + /// + /// Generates a strong password of [length] characters. + /// + /// The length of the password + /// + private string GeneratePassword(int length = 32) + { + RNGCryptoServiceProvider cryptRNG = new RNGCryptoServiceProvider(); + byte[] tokenBuffer = new byte[length]; + cryptRNG.GetBytes(tokenBuffer); + return Convert.ToBase64String(tokenBuffer); + } + + /// + /// Aliases store string names to the Alkami certificate import folder names. + /// + /// String name of the store. + /// + private string GetStoreFolderName(string storename) + { + switch (storename.ToLower()) + { + case "my": + return "Personal"; + + case "ca": + return "IA"; + + case "root": + return "Root"; + + case "trustedpeople": + return "TrustedPeople"; + + default: + throw new Exception("Unknown Store Name Type"); + } + } + + /// + /// Downloads certificates from servers and unzips them to the output directory. + /// + /// + /// + private void DownloadCertificatesFromServers(IEnumerable servers, string outputDirectory) + { + // Export certs from every server. + var serverString = string.Join(",", servers); + using (PowerShell instance = PowerShell.Create()) + { + instance.AddScript(ExportCertificatesScript); + instance.AddParameter("serverString", serverString); + instance.AddParameter("exportPassword", _globalPassword); + instance.AddParameter("importPath", outputDirectory); + var psOutput = instance.Invoke(); + + if (instance.Streams.Error.Any()) + { + Console.WriteLine("There were Errors executing powershell:"); + foreach (var error in instance.Streams.Error) + { + Console.WriteLine(error); + } + } + } + } + + /// + /// Loads all certificates into memory, builds certificate chains, and determines certificate cert/server/environment relationships. + /// + /// The directory of certificates, per server folder. + private void BuildCertificateData(string serverCertificateDirectory, ref Dictionary serverData, ref Dictionary certificateData) + { + var certificateFiles = Directory.GetFiles(serverCertificateDirectory, "*", SearchOption.AllDirectories).Select(p => Path.GetFullPath(p)).ToArray(); + var certificateFileHasPrivateKey = new Dictionary(); + var fileToCertificate = new ConcurrentDictionary(); + + // Load all of the certificates into memory so we can make sense of them. + var options = new ParallelOptions(); + options.MaxDegreeOfParallelism = 32; + foreach (var file in certificateFiles) + { + var extension = Path.GetExtension(file); + + string newPassword = null; + X509Certificate2 cert = null; + if (extension == ".cer") + { + cert = new X509Certificate2(file); + } + else if (extension == ".pfx") + { + cert = new X509Certificate2(file, _globalPassword, X509KeyStorageFlags.Exportable); + + // Generate a unique password for this cert going forward. + newPassword = GeneratePassword(); + } + else + { + throw new Exception($"Unhandled certificate extension {extension}"); + } + + // Store if the particular cert has a private key. + certificateFileHasPrivateKey[file] = cert.HasPrivateKey; + + var certInfo = new CertificateInfo(cert, file, newPassword); + if (!fileToCertificate.TryAdd(file, certInfo)) + { + throw new Exception($"Was unable to track certificate {file}. Investigate."); + } + }; + + // Identify unique certificates from the list of certs from every server which includes duplicates. + // Prefer to select unique certs that include private keys. + var uniqueCertificates = new Dictionary(); + var fileToCertificateEnum = fileToCertificate.Where(cert => cert.Value.Password != null).Concat(fileToCertificate); + foreach (var certInfoKV in fileToCertificateEnum) + { + var certInfo = certInfoKV.Value; + if (!uniqueCertificates.ContainsKey(certInfo.Thumbprint)) + { + uniqueCertificates.Add(certInfo.Thumbprint, certInfo); + } + } + + // Run back through each non-unique certificate, and assign servers/environments to each unique cert. + // This is tracked by the folder that the certificate was loaded from. File paths in the form of: + // "basePath\serverName\certificateStore\certificateName.pfx" + foreach (var file in certificateFiles) + { + // Select the server/store out of the folder path. + var fileSplit = file.Split(new char[] { '\\' }); + var serverName = fileSplit[fileSplit.Length - 3].ToLower(); + var storeName = fileSplit[fileSplit.Length - 2].ToLower(); + + // Look up the unique certificate by the thumbprint of the non-unique certificate. + var nonUniqueCertInfo = fileToCertificate[file]; + var uniqueCertInfo = uniqueCertificates[nonUniqueCertInfo.Thumbprint]; + + // Add the server/environment that the cert is in to the unique cert info object. + var serverInfo = serverData[serverName]; + var environmentName = serverInfo.EnvironmentName.ToLower(); + uniqueCertInfo.Servers.TryAdd(serverName, serverInfo); + uniqueCertInfo.Environments.TryAdd(environmentName, serverInfo.Environment); + + // Record the store that the certificate belongs in, if it does not already exist. + if (!uniqueCertInfo.Stores.Any(certStore => string.Equals(certStore.StoreName, storeName, StringComparison.OrdinalIgnoreCase))) + { + bool storeHasPrivateKey = certificateFileHasPrivateKey[file]; + StoreInfo store = new StoreInfo(storeName, storeHasPrivateKey); + uniqueCertInfo.Stores.Add(store); + } + } + + // Copy all of the unique certificates up to _certificateData, which will be the store of certs for other functions. + foreach (var certInfoKV in uniqueCertificates) + { + certificateData.Add(certInfoKV.Key, certInfoKV.Value); + } + + // For each unique certificate, build out the certificate chain and figure out which certs require which other certs. + // Later we will use this data to create secrets for the "leaf certificates" that are not intermediates of other certificates. + foreach (var certificateInfo in uniqueCertificates) + { + var certificate = certificateInfo.Value.Certificate; + + X509Chain chain = new X509Chain(); + chain.Build(certificate); + + List chainCerts = chain.ChainElements.ToList(); + + // Exit if there's no chain to walk. + if (chainCerts.Count <= 1) + { + continue; + } + + // Build the tree of parent/child certificate associations. + for (int i = 1; i < chainCerts.Count; i++) + { + var parentCert = chainCerts[i]; + var childCert = chainCerts[i - 1]; + + // Apparently certificates in the chain can exist and validate WITHOUT being loaded into the stores of the remote machines. + // They are most likely getting validated through the domain controller. + if (!certificateData.ContainsKey(parentCert.Thumbprint)) + { + string filename = null; // No filename because it wasn't exported from a cert store. + var info = new CertificateInfo(parentCert, filename); + certificateData[parentCert.Thumbprint] = info; + } + + // Attach the parent/child info to certificates for tracking. + var parentInfo = certificateData[parentCert.Thumbprint]; + var childInfo = certificateData[childCert.Thumbprint]; + childInfo.Parent = parentInfo; + parentInfo.Children.Add(childInfo); + } + } + } + + /// + /// Uploads tracked certificates in _certificateData to the secret server at the base path. + /// + /// The base folder path in the secret server to upload certs to. + private void UploadCertificatesToSecretServer(string baseSecretFolder, ref Dictionary environmentData, ref Dictionary certificateData) + { + // Grab the list of certificates to create secrets for. + var certsToExport = certificateData.Select(x => x.Value).ToList(); + + var secretImportZipPath = Path.Combine(_tempPath, "SecretImportTemp"); + if (!Directory.Exists(secretImportZipPath)) + { + Directory.CreateDirectory(secretImportZipPath); + } + + // Create folders on the secret server for every environment, so we can multithread the certificate uploads without fear of stomping. + var majorPodRegex = @"[^\.](\d+)"; + foreach (var environment in environmentData) + { + var type = environment.Value.EnvironmentType; + type = char.ToUpper(type[0]) + type.Substring(1); + + var path = Path.Combine(baseSecretFolder, type); + + // Define the server types. + var serverTypeFolders = new string[] { "Web", "App", "All" }; + + // Create the common folder for the environment type. + foreach (var serverType in serverTypeFolders) + { + var serverTypePath = Path.Combine(path, "Common", serverType); + var folder = _secretServerClient.GetOrAddFolderAsync(serverTypePath).GetAwaiter().GetResult(); + } + + // Only production cares about separate cert folders per environment. + if (type == "Production") + { + var matches = Regex.Matches(environment.Value.Name, majorPodRegex); + string majorPod = matches[0].Captures[0].Value.Trim(); + path = Path.Combine(path, majorPod); + } + + // Server type folders inside the pod. + foreach (var serverType in serverTypeFolders) + { + var serverTypePath = Path.Combine(path, serverType); + var folder = _secretServerClient.GetOrAddFolderAsync(serverTypePath).GetAwaiter().GetResult(); + } + } + + // Fetch all the secrets from the base folder, so we can check for secrets we've already loaded. + bool includeSubfolders = true; + var baseFolder = _secretServerClient.GetOrAddFolderAsync(baseSecretFolder).GetAwaiter().GetResult(); + var allSecrets = _secretServerClient.GetSecretsByFolder(baseFolder, includeSubfolders); + + try + { + var secretTemplateType = _secretServerClient.GetSecretTemplateByName(CertificateTemplateTypeName); + + // Create separate .zip individual files for each certificate being uploaded. + var options = new ParallelOptions(); + options.MaxDegreeOfParallelism = 32; + Parallel.ForEach(certsToExport, options, (cert) => + { + // Create a folder for the cert. + var certFolderPath = Path.Combine(secretImportZipPath, cert.Thumbprint); + if (!Directory.Exists(certFolderPath)) + { + Directory.CreateDirectory(certFolderPath); + } + + // Copy all of the certs for the chain + var currentCert = cert; + Console.WriteLine($"Working on {cert.Name}"); + do + { + // Don't export this cert because it didn't exist in the stores of the remote machines. + if (cert.FileName == null) + { + currentCert = currentCert.Parent; + continue; + } + + // For each store the cert belongs in. + foreach (var store in cert.Stores) + { + // Export certificates to the appropriate place and store folder. + var outputDirectory = Path.Combine(certFolderPath, store.StoreName); + if (!Directory.Exists(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + + var password = store.HasPrivateKey ? cert.Password : null; + CertificateHelper.ExportCertificate(cert.Certificate, outputDirectory, password); + } + + // Move along to the next cert in the chain. + currentCert = currentCert.Parent; + } while (currentCert != null); + + // Zip up the cert for upload to secret. + var outputZipPath = Path.Combine(secretImportZipPath, $"{cert.Thumbprint}.zip"); + ZipFile.CreateFromDirectory(certFolderPath, outputZipPath); + + var currentZipHash = ""; + using (var stream = File.OpenRead(outputZipPath)) + { + currentZipHash = Extensions.GetMd5HashString(stream); + } + + // Figure out what environment subfolder the certificate is going in on the secret server. + var serverNames = cert.Servers.Select(serverInfoKV => serverInfoKV.Key.ToLower()); + + var appServerNames = new string[] { "vma", "app", "fab", "mic" }; + var webServerNames = new string[] { "vmw", "web" }; + bool onApps = appServerNames.Any(type => serverNames.Any(serverName => serverName.Contains(type))); + bool onWebs = webServerNames.Any(type => serverNames.Any(serverName => serverName.Contains(type))); + bool onBothWebsAndApps = onApps && onWebs; + + // Determine the types of environments that this cert exists in. + // We duplicate certs for each environment. It's too reckless to have a "every environment type" common folder. + var environmentTypes = cert.Environments + .Select(e => e.Value.EnvironmentType.ToLower()) + .Distinct() + .Select(et => char.ToUpper(et.ElementAt(0)) + et.Substring(1)); // Upper case environment type names. + + foreach (var environmentType in environmentTypes) + { + // Build secret server folder path to create the secret. + + // Determine if the cert is on multiple pods within an environment type. + var environments = cert.Environments.Where(e => e.Value.EnvironmentType.ToLower() == environmentType.ToLower()); + bool onMultiplePods = environments.Count() >= 2; + + var secretServerFolderPath = Path.Combine(baseSecretFolder, environmentType); + + if (onMultiplePods) + { + secretServerFolderPath = Path.Combine(secretServerFolderPath, "Common"); + } + else if (environmentType == "Production") + { + var matches = Regex.Matches(environments.First().Value.Name, majorPodRegex); + string majorPod = matches[0].Captures[0].Value.Trim(); + + secretServerFolderPath = Path.Combine(secretServerFolderPath, majorPod); + } + + string serverFolder = string.Empty; + if (onBothWebsAndApps) + { + serverFolder = "All"; + } + else if (onWebs) + { + serverFolder = "Web"; + } + else + { + serverFolder = "App"; + } + + secretServerFolderPath = Path.Combine(secretServerFolderPath, serverFolder); + + var fullSecretPath = Path.Combine(secretServerFolderPath, cert.UniqueName); + var folder = _secretServerClient.GetOrAddFolderAsync(secretServerFolderPath).GetAwaiter().GetResult(); + + // Figure out if the secret already exists for the cert. + var secretName = cert.UniqueName; + if (allSecrets.Any(existingSecret => existingSecret.FolderID == folder.ID && string.Equals(existingSecret.Name, secretName, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + // Create the secret. + var secretStub = _secretServerClient.CreateSecretStubByTemplateIDAsync(secretTemplateType.ID, secretName + "-" + folder.ID, folder.ID).GetAwaiter().GetResult(); + var expiration = cert.Certificate.NotAfter.ToString(); + + // Fill out details about the cert. + secretStub["Import Password"] = cert.Password; + secretStub["Certificate Name"] = secretName; + secretStub["Thumbprint"] = cert.Thumbprint; + secretStub["ExpirationDate"] = expiration; + secretStub["File Hash"] = currentZipHash; + + var foundCert = _secretServerClient.FindSecretByName(secretName); + if (foundCert != null) + { + Console.WriteLine($"Secret exists {secretName}"); + } + else + { + try + { + // Create the secret. + var secret = _secretServerClient.CreateSecret(secretStub).GetAwaiter().GetResult(); + + // Upload the cert .zip. + _secretServerClient.UploadFile(secret, outputZipPath).GetAwaiter().GetResult(); + } + catch (Exception e) + { + Console.WriteLine("{0} Exception caught.", e.Message); + } + } + } + }); + } + finally + { + // Clean up the cert directory. + if (Directory.Exists(secretImportZipPath)) + { + Directory.Delete(secretImportZipPath, true); + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Utilities/Extensions.cs b/Modules/Alkami.Ops.Certificates/Utilities/Extensions.cs new file mode 100644 index 0000000..d3e897c --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Utilities/Extensions.cs @@ -0,0 +1,304 @@ +using Alkami.Ops.Certificates.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Xml; + +namespace Alkami.Ops.Certificates.Utilities +{ + internal static class Extensions + { + /// + /// Returns a List of certificates from an X509Certificate2Collection + /// + /// + public static List ToList(this X509Certificate2Collection collection) + { + var list = new List(collection.Count); + foreach (var cert in collection) + { + list.Add(cert); + } + return list; + } + + /// + /// Returns a List of certificates from an X509Certificate2Collection + /// + /// + public static List ToList(this X509ChainElementCollection collection) + { + var list = new List(collection.Count); + foreach (var chainelement in collection) + { + list.Add(chainelement.Certificate); + } + return list; + } + + /// + /// Adds a value to a dictionary if it doesn't exist and returns true. Returns false if the key already exists. + /// + /// + /// + /// + /// The dictionary key to set. + /// The value to store in the dictionary. + /// + public static bool TryAdd(this Dictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + else + { + dictionary[key] = value; + return true; + } + } + + /// + /// Deletes all files/directories in a directory without deleting the directory itself. + /// + /// + public static void ClearDirectory(string directory) + { + if (!Directory.Exists(directory)) + { + return; + } + + foreach (var file in Directory.GetFiles(directory)) + { + File.Delete(file); + } + + foreach (var dir in Directory.GetDirectories(directory)) + { + foreach (var file in Directory.GetFiles(dir, "*", SearchOption.AllDirectories)) + { + File.Delete(file); + } + + Directory.Delete(dir, true); + } + } + + /// + /// Returns true if the two files are equal. + /// + /// First file to compare. + /// Second file to compare. + /// + public static bool FilesAreEqual(FileInfo first, FileInfo second) + { + const int BYTES_TO_READ = sizeof(Int64); + if (first.Length != second.Length) + { + return false; + } + + if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + int iterations = (int)Math.Ceiling((double)first.Length / BYTES_TO_READ); + + using (FileStream fs1 = first.OpenRead()) + using (FileStream fs2 = second.OpenRead()) + { + byte[] one = new byte[BYTES_TO_READ]; + byte[] two = new byte[BYTES_TO_READ]; + + for (int i = 0; i < iterations; i++) + { + fs1.Read(one, 0, BYTES_TO_READ); + fs2.Read(two, 0, BYTES_TO_READ); + + if (BitConverter.ToInt64(one, 0) != BitConverter.ToInt64(two, 0)) + { + return false; + } + } + } + + return true; + } + + /// + /// Loads an EnvironmentInfo given a server to read properties from. + /// + /// Server name. + /// + public static ServerInfo GetServerInfo(string serverName) + { + const string MachineConfigUncPath = "\\\\{0}\\C$\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\Config\\machine.config"; + + string machineConfigPath = string.Format(MachineConfigUncPath, serverName); + if (!File.Exists(machineConfigPath)) + { + throw new Exception($"Could not locate valid machine config for server {serverName}"); + } + + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.IgnoreWhitespace = true; + readerSettings.IgnoreComments = true; + readerSettings.CheckCharacters = true; + readerSettings.CloseInput = true; + readerSettings.IgnoreProcessingInstructions = false; + readerSettings.ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None; + readerSettings.ValidationType = ValidationType.None; + XmlReader reader = XmlReader.Create(machineConfigPath, readerSettings); + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + string environmentName = null; + string environmentType = null; + string hostingProvider = null; + string serverType = null; + string microUser = null; + string dbUser = null; + + var appSettingNode = doc.SelectSingleNode("//appSettings"); + if (appSettingNode.HasChildNodes) + { + var appSettings = appSettingNode.ChildNodes; + foreach (XmlNode setting in appSettings) + { + string key = setting.Attributes["key"].InnerText.ToLower(); + string value = setting.Attributes["value"].InnerText; + switch (key) + { + case "environment.name": + environmentName = value; + break; + + case "environment.type": + environmentType = value; + break; + + case "environment.hosting": + hostingProvider = value; + break; + + case "environment.server": + serverType = value; + break; + + case "databasemicroserviceaccount": + dbUser = value; + break; + + case "nondatabasemicroserviceaccount": + microUser = value; + break; + } + } + } + + // Return a null result if any piece of information is missing. + if (environmentName == null || + environmentType == null || + hostingProvider == null || + serverType == null || + microUser == null || + dbUser == null) + { + return null; + } + + return new ServerInfo(environmentName, environmentType, hostingProvider, serverType, microUser, dbUser, null); + } + + /// + /// Returns the proper StoreName enum given the folder name for the store used in our current cert automation. + /// + /// + /// + public static StoreName GetStoreNameByFolderName(string folderName) + { + StoreName storeName; + switch (folderName) + { + case "ia": + storeName = StoreName.CertificateAuthority; + break; + + case "personal": + storeName = StoreName.My; + break; + + case "root": + storeName = StoreName.Root; + break; + + case "trustedpeople": + storeName = StoreName.TrustedPeople; + break; + + default: + throw new Exception($"Could not identify store name {folderName}"); + } + + return storeName; + } + + /// + /// Stolen shamelessly from the md5 documentation https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.md5?view=netframework-4.8 + /// Modified to accept a Stream rather than converting a string. + /// + /// Data stream to hash + /// Calculated md5 hash + public static string GetMd5HashString(Stream stream) + { + var md5Hash = MD5.Create(); + // Convert the input string to a byte array and compute the hash. + byte[] data = md5Hash.ComputeHash(stream); + + // Create a new Stringbuilder to collect the bytes + // and create a string. + StringBuilder sBuilder = new StringBuilder(); + + // Loop through each byte of the hashed data + // and format each one as a hexadecimal string. + for (int i = 0; i < data.Length; i++) + { + sBuilder.Append(data[i].ToString("x2")); + } + + // Return the hexadecimal string. + return sBuilder.ToString(); + } + + /// + /// Stolen shamelessly from the md5 documentation https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.md5?view=netframework-4.8 + /// Modified to accept a Stream to hash rather than converting a string. + /// + /// Stream to hash and compare against provided hash string + /// Hash to match against. + /// + public static bool VerifyMd5Hash(Stream stream, string hash) + { + var md5Hash = MD5.Create(); + // Hash the input. + string hashOfInput = GetMd5HashString(stream); + + // Create a StringComparer an compare the hashes. + StringComparer comparer = StringComparer.OrdinalIgnoreCase; + + if (0 == comparer.Compare(hashOfInput, hash)) + { + return true; + } + else + { + return false; + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/Utilities/QuickWatch.cs b/Modules/Alkami.Ops.Certificates/Utilities/QuickWatch.cs new file mode 100644 index 0000000..1b66e5d --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/Utilities/QuickWatch.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics; + +namespace Alkami.Ops.Certificates.Utilities +{ + /// + /// A convenience class that writes stopwatch start/stop messages inside using blocks. + /// + internal class QuickWatch : IDisposable + { + public readonly string Name; + public readonly Stopwatch StopWatch; + + public QuickWatch(string name) + { + Name = name; + StopWatch = new Stopwatch(); + Console.WriteLine($"Starting [{Name}]"); + StopWatch.Start(); + } + + public void Dispose() + { + Console.WriteLine($"[{Name}] completed in {StopWatch.Elapsed}"); + StopWatch.Stop(); + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/packages.config b/Modules/Alkami.Ops.Certificates/packages.config new file mode 100644 index 0000000..c10cc67 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/tools/chocolateyInstall.ps1 b/Modules/Alkami.Ops.Certificates/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Certificates/tools/chocolateyUninstall.ps1 b/Modules/Alkami.Ops.Certificates/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.Ops.Certificates/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.Ops.Common.Testing/Alkami.Ops.Common.Testing.csproj b/Modules/Alkami.Ops.Common.Testing/Alkami.Ops.Common.Testing.csproj new file mode 100644 index 0000000..365a0ec --- /dev/null +++ b/Modules/Alkami.Ops.Common.Testing/Alkami.Ops.Common.Testing.csproj @@ -0,0 +1,73 @@ + + + + + + Debug + AnyCPU + {4FD5CC3A-28F4-4BBE-8049-2B9725E7C362} + Library + Properties + Alkami.Ops.Common.Testing + Alkami.Ops.Common.Testing + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\Modules\packages\NUnit.3.10.1\lib\net45\nunit.framework.dll + + + + + + + + + + + + + + + + {fa9745dd-68ac-4194-9c33-acf19411d357} + Alkami.Ops.Common + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common.Testing/Integration/ZipHelperIntegrationTests.cs b/Modules/Alkami.Ops.Common.Testing/Integration/ZipHelperIntegrationTests.cs new file mode 100644 index 0000000..fad1092 --- /dev/null +++ b/Modules/Alkami.Ops.Common.Testing/Integration/ZipHelperIntegrationTests.cs @@ -0,0 +1,258 @@ +using Alkami.Ops.Common.FileSystem; +using NUnit.Framework; +using System; +using System.IO; +using System.IO.Compression; + +namespace Alkami.Ops.Common.Integration.Testing +{ + [TestFixture] + public class ZipHelperIntegrationTests + { + private string tempPath; + + [SetUp] + public void Setup() + { + this.tempPath = Path.Combine(Path.GetTempPath() + "ZipTests"); + } + + [Test] + public void WhenCallingUnzip_WithDestinationDirectory_FilesAreUnzippedAtDestinationLocation() + { + // Arrange + var zipPath = this.CreateRandomizedZip(); + var destinationDirectory = Path.Combine(Path.GetDirectoryName(zipPath), "output"); + + using (var zipArchive = ZipFile.OpenRead(zipPath)) + { + Directory.CreateDirectory(destinationDirectory); + + // Act + ZipHelpers.UnZip(zipPath, destinationDirectory); + + // Assert + foreach (var entry in zipArchive.Entries) + { + var attributes = File.GetAttributes(Path.Combine(destinationDirectory, entry.FullName)); + + if (attributes.HasFlag(FileAttributes.Directory)) + { + Assert.That(Directory.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + } + } + // Test fails if we can't re-run it + Assert.That(this.CleanupFiles(destinationDirectory)); + } + + [Test] + public void WhenCallingUnzip_WithNoDestinationDirectory_FilesAreUnzippedLocally() + { + // Arrange + var zipPath = this.CreateRandomizedZip(); + var destinationDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + + using (var zipArchive = ZipFile.OpenRead(zipPath)) + { + // Act + ZipHelpers.UnZip(zipPath); + + destinationDirectory = Path.Combine(destinationDirectory, Path.GetFileNameWithoutExtension(zipPath)); + + // Assert + foreach (var entry in zipArchive.Entries) + { + var attributes = File.GetAttributes(Path.Combine(destinationDirectory, entry.FullName)); + + if (attributes.HasFlag(FileAttributes.Directory)) + { + Assert.That(Directory.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + } + } + + // Test fails if we can't re-run it + Assert.That(this.CleanupFiles(destinationDirectory)); + } + + [Test] + public void WhenCallingUnZipSubDirectory_WithDestinationDirectory_OnlyFilesFromSubDirectoryAreUnzippedAtDestinationLocation() + { + // Arrange + var zipPath = this.CreateRandomizedZip(); + var destinationDirectory = Path.Combine(Path.GetDirectoryName(zipPath), "output"); + var internalDirectory = "subOne"; + + using (var zipArchive = ZipFile.OpenRead(zipPath)) + { + Directory.CreateDirectory(destinationDirectory); + + // Act + ZipHelpers.UnZipSubDirectory(internalDirectory, zipPath, destinationDirectory); + + // Assert + foreach (var entry in zipArchive.Entries) + { + if (Path.GetDirectoryName(entry.FullName).Contains(internalDirectory)) + { + var attributes = File.GetAttributes(Path.Combine(destinationDirectory, entry.FullName)); + + if (attributes.HasFlag(FileAttributes.Directory)) + { + Assert.That(Directory.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.Name)), Is.False); + } + } + } + // Test fails if we can't re-run it + Assert.That(this.CleanupFiles(destinationDirectory)); + } + + [Test] + public void WhenCallingUnZipSubDirectory_WithNoDestinationDirectory_OnlyFilesFromSubDirectoryAreUnzippedLocally() + { + // Arrange + var zipPath = this.CreateRandomizedZip(); + var destinationDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var internalDirectory = "subOne"; + + using (var zipArchive = ZipFile.OpenRead(zipPath)) + { + Directory.CreateDirectory(destinationDirectory); + + // Act + ZipHelpers.UnZipSubDirectory(internalDirectory, zipPath); + + destinationDirectory = Path.Combine(destinationDirectory, Path.GetFileNameWithoutExtension(zipPath)); + + // Assert + foreach (var entry in zipArchive.Entries) + { + if (Path.GetDirectoryName(entry.FullName).Contains(internalDirectory)) + { + var attributes = File.GetAttributes(Path.Combine(destinationDirectory, entry.FullName)); + + if (attributes.HasFlag(FileAttributes.Directory)) + { + Assert.That(Directory.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.FullName))); + } + } + else + { + Assert.That(File.Exists(Path.Combine(destinationDirectory, entry.Name)), Is.False); + } + } + } + // Test fails if we can't re-run it + Assert.That(this.CleanupFiles(destinationDirectory)); + } + + private bool CleanupFiles(string destinationDirectory) + { + try + { + if (Directory.Exists(this.tempPath)) + Directory.Delete(this.tempPath, true); + if (Directory.Exists(destinationDirectory)) + Directory.Delete(destinationDirectory, true); + if (File.Exists(Path.Combine(Path.GetDirectoryName(this.tempPath), "tempFile.zip"))) + File.Delete(Path.Combine(Path.GetDirectoryName(this.tempPath), "tempFile.zip")); + + return true; + } + catch (Exception ex) + { + Console.WriteLine("Problem cleaning up. " + ex.Message); + } + + return false; + } + + private string CreateRandomizedZip() + { + // Create Directories. Top level + 2 sub directories + var topLevelDirectory = Directory.CreateDirectory(this.tempPath); + var subDirectoryOne = Directory.CreateDirectory(Path.Combine(this.tempPath, "subOne")); + var subDirectoryTwo = Directory.CreateDirectory(Path.Combine(this.tempPath, "subTwo")); + var subSubDirectory = Directory.CreateDirectory(Path.Combine(subDirectoryOne.FullName, "subSub")); + + // Create 2 files in each directory + + using (var handle = File.CreateText(Path.Combine(topLevelDirectory.FullName, Path.GetRandomFileName()))) + { + handle.WriteLine("I am the first test file"); + handle.Flush(); + } + + using (var handle = File.CreateText(Path.Combine(topLevelDirectory.FullName, Path.GetRandomFileName()))) + { + handle.WriteLine("I am the second test file"); + handle.Flush(); + } + + foreach (var directory in topLevelDirectory.EnumerateDirectories()) + { + using (var handle = File.CreateText(Path.Combine(directory.FullName, Path.GetRandomFileName()))) + { + handle.WriteLine("I am the first test file"); + handle.Flush(); + } + + using (var handle = File.CreateText(Path.Combine(directory.FullName, Path.GetRandomFileName()))) + { + handle.WriteLine("I am the second test file"); + handle.Flush(); + } + + using (var handle = File.CreateText(Path.Combine(directory.FullName, "subOne.txt"))) + { + handle.WriteLine("I am a file which matches the target directory name."); + handle.Flush(); + } + } + + // Create a file in a secondary sub folder. + using (var handle = File.CreateText(Path.Combine(subSubDirectory.FullName, Path.GetRandomFileName()))) + { + handle.WriteLine("I am the Sub Sub test file"); + handle.Flush(); + } + + // Create an empty sub directory. + var empty = Directory.CreateDirectory(Path.Combine(subDirectoryOne.FullName, "EmptyDirectory")); + + // Zip up the top level directory + var zipPath = Path.Combine(Path.GetDirectoryName(this.tempPath), "tempFile.zip"); + + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + ZipFile.CreateFromDirectory(this.tempPath, zipPath); + + return zipPath; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common.Testing/Properties/AssemblyInfo.cs b/Modules/Alkami.Ops.Common.Testing/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..515f59c --- /dev/null +++ b/Modules/Alkami.Ops.Common.Testing/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Alkami.Ops.Common.Testing")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Alkami.Ops.Common.Testing")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("4fd5cc3a-28f4-4bbe-8049-2b9725e7c362")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Modules/Alkami.Ops.Common.Testing/packages.config b/Modules/Alkami.Ops.Common.Testing/packages.config new file mode 100644 index 0000000..a14066b --- /dev/null +++ b/Modules/Alkami.Ops.Common.Testing/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/Alkami.Ops.Common.csproj b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.csproj new file mode 100644 index 0000000..bd078f2 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {FA9745DD-68AC-4194-9C33-ACF19411D357} + Library + Properties + Alkami.Ops.Common + Alkami.Ops.Common + v4.6.2 + 512 + ..\ + true + + + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + true + false + + + bin\Release\ + TRACE + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + + + OnOutputUpdated + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/Alkami.Ops.Common.nuspec b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.nuspec new file mode 100644 index 0000000..28fab17 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.nuspec @@ -0,0 +1,20 @@ + + + + Alkami.Ops.Common + $version$ + Alkami Technology + Alkami Technology + false + Alkami.Ops.Common + + + + + + + + + + + diff --git a/Modules/Alkami.Ops.Common/Alkami.Ops.Common.psd1 b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.psd1 new file mode 100644 index 0000000..782cee3 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Alkami.Ops.Common.psd1 @@ -0,0 +1,18 @@ +@{ + RootModule = 'Alkami.Ops.Common.psm1' + ModuleVersion = '4.1.2' + GUID = '0bcdfeb2-09e8-4760-aaf2-232597794ccb' + Author = 'SRE,dsage,cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'Common functions' + PowerShellVersion = '5.0' + PrivateData = @{ + PSData = @{ + Tags = @('Ops.Common') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.Ops.Common' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Common' +} diff --git a/Modules/Alkami.Ops.Common/AlkamiManifest.xml b/Modules/Alkami.Ops.Common/AlkamiManifest.xml new file mode 100644 index 0000000..ff70252 --- /dev/null +++ b/Modules/Alkami.Ops.Common/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.Ops.Common + SREModule + + + Production + + diff --git a/Modules/Alkami.Ops.Common/Cryptography/CertificateHelper.cs b/Modules/Alkami.Ops.Common/Cryptography/CertificateHelper.cs new file mode 100644 index 0000000..daf96c2 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Cryptography/CertificateHelper.cs @@ -0,0 +1,713 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using Alkami.Ops.Common.Exceptions; +using Alkami.Ops.Common.Extensions; +using Alkami.Ops.Common.NativeMethods; + +namespace Alkami.Ops.Common.Cryptography +{ + public static class CertificateHelper + { + private const int CertStoreProvSystem = 10; + private const int CertSystemStoreCurrentUser = (1 << 16); + private const int CertSystemStoreLocalMachine = (2 << 16); + + /// + /// Finds a Certificate By Thumbprint + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static X509Certificate2 FindCertificateByThumbprint(string thumbPrint, StoreName storeName, + StoreLocation location, string hostName, string machineName = null) + { + var searchLocal = + string.Equals(hostName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) || + string.Equals(machineName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) || + string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase); + + if (searchLocal) + { + return SearchLocalStore(thumbPrint, storeName, location, X509FindType.FindByThumbprint); + } + + return SearchRemoteStoreByThumbprint(thumbPrint, storeName, location, hostName); + } + + /// + /// Finds the WMSvc Certificate for the local machine + /// + /// + public static X509Certificate2 FindIISIssuedCertificate() + { + var certificates = GetAllCertificates(StoreName.My, StoreLocation.LocalMachine, string.Empty); + + foreach (var certificate in certificates) + { + var friendlyName = certificate.FriendlyName; + + if (friendlyName.StartsWith("WMSVC", StringComparison.OrdinalIgnoreCase)) + { + return certificate; + } + } + + return null; + } + + /// + /// Searches for Certificates by Subject or Subject Alternate Name + /// + /// + /// Additionally searches for a "wildcard" match, such as *.dev.alkamitech.com + /// + /// + /// + /// + /// + /// + public static X509Certificate2 FindCertificatebySubjectOrSAN(string subjectOrSAN, StoreName storeName, StoreLocation storeLocation, string machineName = null) + { + var segments = subjectOrSAN.Split('.'); + segments[0] = "*"; + var wildcardUri = string.Join(".", segments); + + // If all we got is a *, we won't search for wildcards, since it would be unreliable + var searchWildCard = (wildcardUri != "*"); + + var certificates = GetAllCertificates(storeName, storeLocation, machineName); + + foreach (var certificate in certificates) + { + var subjectAlternateNames = certificate.Extensions.OfType().FirstOrDefault(e => e.Oid.FriendlyName == "Subject Alternative Name"); + var subject = certificate.GetNameInfo(X509NameType.SimpleName, false); + + if (subjectAlternateNames != null) + { + var asnData = new AsnEncodedData(subjectAlternateNames.Oid, subjectAlternateNames.RawData); + if (asnData.Format(true).IndexOf(subjectOrSAN, StringComparison.OrdinalIgnoreCase) >= 0 || + (searchWildCard && asnData.Format(true).IndexOf(wildcardUri, StringComparison.OrdinalIgnoreCase) >= 0)) + { + return certificate; + } + } + + if ((subject != null) && (subject.IndexOf(subjectOrSAN, StringComparison.OrdinalIgnoreCase) >= 0 || + (searchWildCard && subject.IndexOf(wildcardUri, StringComparison.OrdinalIgnoreCase) >= 0))) + { + return certificate; + } + } + + return null; + } + + /// + /// Loads a Certificate in to the Specified Store + /// + /// + /// + /// + /// + /// + /// + public static void LoadCertificateToStore(X509Certificate2 certificate, StoreName storeName, StoreLocation location) + { + var store = new X509Store(storeName, location); + + try + { + store.Open(OpenFlags.ReadWrite); + store.Add(certificate); + } + finally + { + store.Close(); + } + } + + /// + /// Loads a Certificate in to the Specified Store + /// + /// + /// + /// + /// + /// + /// + /// + public static void LoadCertificateToStore(string certificatePath, StoreName storeName, StoreLocation location, string password = null) + { + X509KeyStorageFlags flags; + + if (location == StoreLocation.LocalMachine) + { + flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet; + } + else + { + flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet; + } + + + var certificate = !string.IsNullOrEmpty(password) + ? new X509Certificate2(certificatePath, password, flags) + : new X509Certificate2(certificatePath); + + LoadCertificateToStore(certificate, storeName, location); + } + + /// + /// Loads the Certificate, discarding the private key, to a store + /// + /// + /// + /// + public static void LoadCertificateFromPFXToStore(string certificatePath, StoreName storeName, StoreLocation location) + { + var certificateOnly = new X509Certificate2(certificatePath); + LoadCertificateToStore(certificateOnly, storeName, location); + } + + /// + /// Removes a certificate or certificates from a store by name or thumbprint + /// + /// + /// + /// + public static void RemoveCertificateFromStore(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location) + { + var store = new X509Store(storeName, location); + + try + { + store.Open(OpenFlags.ReadWrite); + + var certificateCollection = store.Certificates.Find( + certificateNameOrThumbprint.IsBase64String() + ? X509FindType.FindByThumbprint + : X509FindType.FindBySubjectName, + certificateNameOrThumbprint, false); + + store.RemoveRange(certificateCollection); + } + finally + { + store.Close(); + } + } + + /// + /// Validates a certificate chain and effective and expiration dates + /// + /// + /// + /// + public static void ValidateCertificate(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location) + { + var store = new X509Store(storeName, location); + + try + { + store.Open(OpenFlags.ReadWrite); + + var certificateCollection = store.Certificates.Find( + certificateNameOrThumbprint.IsBase64String() + ? X509FindType.FindByThumbprint + : X509FindType.FindBySubjectName, + certificateNameOrThumbprint, false); + + foreach (var certificate in certificateCollection) + { + DateTime effectiveDate; + if (!DateTime.TryParse(certificate.GetEffectiveDateString(), out effectiveDate)) + { + throw new InvalidCertificateException("Could not read certificate effective date", certificate.FriendlyName, + certificate.Thumbprint); + } + + DateTime expirationDate; + if (!DateTime.TryParse(certificate.GetExpirationDateString(), out expirationDate)) + { + throw new InvalidCertificateException("Could not read certificate expiration date", certificate.FriendlyName, + certificate.Thumbprint); + } + + if (effectiveDate > DateTime.Now) + { + var message = $"Certificate effective date {effectiveDate:yyyy-mm-dd hh:mm:ss} is not yet in effect"; + throw new InvalidCertificateException(message, certificate.FriendlyName, certificate.Thumbprint, effectiveDate, expirationDate); + + } + + if (expirationDate < DateTime.Now) + { + var message = $"Certificate expiration date {expirationDate:yyyy-mm-dd hh:mm:ss} has already passed"; + throw new InvalidCertificateException(message, certificate.FriendlyName, certificate.Thumbprint, effectiveDate, expirationDate); + } + + if (!certificate.Verify()) + { + throw new InvalidCertificateException("Certificate chain validation failed", certificate.FriendlyName, certificate.Thumbprint, + effectiveDate, expirationDate); + } + } + } + finally + { + store.Close(); + } + } + + /// + /// Grants rights to private key files on disk + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void GrantRightsToPrivateKeys(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location, string user) + { + var cert = SearchLocalStore(certificateNameOrThumbprint, storeName, location, certificateNameOrThumbprint.IsBase64String() + ? X509FindType.FindByThumbprint + : X509FindType.FindBySubjectName); + + var rsaKey = cert.PrivateKey as RSACryptoServiceProvider; + if (rsaKey == null) + { + throw new PrivateKeyNotFoundException( + "The key file could not be found for the certificate", + cert.GetNameInfo(X509NameType.SimpleName, false), + cert.Thumbprint, + storeName.ToString(), + location.ToString() + ); + } + + var keyFilePath = FindKeyLocation(rsaKey.CspKeyContainerInfo.UniqueKeyContainerName); + var pkFile = new FileInfo(Path.Combine(keyFilePath, rsaKey.CspKeyContainerInfo.UniqueKeyContainerName)); + + GrantRightsToPrivateKeys(pkFile, user); + } + + /// + /// Export a Certificate by Name or Thumbprint + /// + /// + /// + /// + /// + /// + public static void ExportCertificate(string certificateNameOrThumbprint, StoreName storeName, StoreLocation location, string exportPath, string password = null) + { + var cert = SearchLocalStore(certificateNameOrThumbprint, storeName, location, certificateNameOrThumbprint.IsBase64String() + ? X509FindType.FindByThumbprint + : X509FindType.FindBySubjectName); + + ExportCertificate(cert, exportPath, password); + } + + /// + /// Exports a specific certificate to the destination exportPath folder. + /// + /// The certificate to be exported. + /// The path of the folder to export the certificate to. + /// Optional password for the certificate. + public static void ExportCertificate(X509Certificate2 certificate, string exportPath, string password = null) + { + // Don't export expired certificates. + bool expired = DateTime.Now > certificate.NotAfter; + if (expired) + { + return; + } + + bool hasPassword = !string.IsNullOrEmpty(password); + + // Get the byte data of the certificate. + byte[] bytes; + string extension; + if (hasPassword) + { + if (!certificate.HasPrivateKey) + { + extension = ".cer"; + bytes = certificate.Export(X509ContentType.Cert); + } + else + { + + extension = ".pfx"; + bytes = certificate.Export(X509ContentType.Pkcs12, password); + } + } + else + { + extension = ".cer"; + bytes = certificate.Export(X509ContentType.Cert); + } + + // Determine the name of the file by friendly name and thumbprint for uniqueness. + string certName = certificate.GetNameInfo(X509NameType.SimpleName, false).GetSanitizedFileName() + "-" + certificate.Thumbprint; + + // Save the cert. + var exportFileName = Path.Combine(exportPath, certName + extension); + File.WriteAllBytes(exportFileName, bytes); + } + + /// + /// Export All Certificates from a Specific Store + /// + /// + /// + /// + /// + public static List ExportAllCertificates(StoreName storeName, StoreLocation location, string exportPath, string password = null) + { + var store = new X509Store(storeName, location); + var exceptionList = new List(); + + try + { + store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); + + foreach (var cert in store.Certificates) + { + try + { + ExportCertificate(cert, exportPath, password); + } + catch (Exception ex) + { + exceptionList.Add(new CertificateExportException("Could not Export Certificate Due to Errors", + cert.GetNameInfo(X509NameType.SimpleName, false), cert.SubjectName.Name, + cert.Thumbprint, ex.Message)); + } + } + } + finally + { + store.Close(); + } + + return exceptionList; + } + + /// + /// Returns all Certificates from the Specified Store as an X509Certificate2Collection + /// + /// + /// + /// + /// + public static X509Certificate2Collection GetAllCertificates(StoreName storeName, StoreLocation location, string machineName = null) + { + return (string.Equals(machineName, Environment.MachineName, StringComparison.OrdinalIgnoreCase) || + string.IsNullOrEmpty(machineName)) + ? GetCertificatesFromLocalStore(storeName, location) + : GetCertificatesFromRemoteStore(storeName, location, machineName); + } + + /// + /// Returns Certificates from the Specified Remote Store + /// + /// + /// + /// + /// + private static X509Certificate2Collection GetCertificatesFromRemoteStore(StoreName storeName, StoreLocation location, string machineName) + { + var safeHandle = IntPtr.Zero; + + try + { + safeHandle = GetRemoteCertificateStore(storeName, location, machineName, safeHandle); + + if (safeHandle == IntPtr.Zero) + { + return new X509Certificate2Collection(); + } + + var currentCertContext = IntPtr.Zero; + + var store = new X509Store("temp"); + store.Open(OpenFlags.ReadWrite); + + do + { + currentCertContext = SafeNativeMethods.CertEnumCertificatesInStore(safeHandle, currentCertContext); + + if (currentCertContext == IntPtr.Zero) + { + continue; + } + + store.Add(new X509Certificate2(currentCertContext)); + + } while (currentCertContext != (IntPtr)0); + + return store.Certificates; + } + finally + { + if (safeHandle != IntPtr.Zero) + { + SafeNativeMethods.CertCloseStore(safeHandle, 0); + } + } + } + + /// + /// Returns Certificates from the Specified Local Store + /// + /// + /// + /// + private static X509Certificate2Collection GetCertificatesFromLocalStore(StoreName storeName, StoreLocation location) + { + var store = new X509Store(storeName, location); + + try + { + store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); + return store.Certificates; + } + finally + { + store.Close(); + } + } + + /// + /// Grants rights to private key files on disk + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void GrantRightsToPrivateKeys(FileInfo pkFile, string user) + { + if (!pkFile.Exists) + { + return; + } + + var fs = pkFile.GetAccessControl(); + + var account = new NTAccount(user); + fs.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.FullControl, AccessControlType.Allow)); + + pkFile.SetAccessControl(fs); + } + + /// + /// Safely Searches a Local Certificate Store + /// + /// + /// + /// + /// + /// + private static X509Certificate2 SearchLocalStore(string name, StoreName storeName, StoreLocation location, X509FindType findType) + { + var store = new X509Store(storeName, location); + + try + { + store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); + + return store.Certificates.Find(findType, name, false) + .OfType() + .FirstOrDefault(); + } + finally + { + store.Close(); + } + } + + /// + /// Searches a Remote Store By Thumbprint + /// + /// + /// + /// + /// + /// + /// a X509Certificate2 + private static X509Certificate2 SearchRemoteStoreByThumbprint(string thumbPrint, StoreName storeName, StoreLocation location, string machineName, string password = null) + { + var safeHandle = IntPtr.Zero; + try + { + safeHandle = GetRemoteCertificateStore(storeName, location, machineName, safeHandle); + + if (safeHandle != IntPtr.Zero) + { + var currentCertContext = IntPtr.Zero; + + do + { + currentCertContext = SafeNativeMethods.CertEnumCertificatesInStore(safeHandle, + currentCertContext); + + if (currentCertContext == IntPtr.Zero) + { + continue; + } + + if (string.IsNullOrEmpty(password)) + { + var cert = new X509Certificate2(currentCertContext); + + if (string.Equals(cert.Thumbprint, thumbPrint, StringComparison.InvariantCultureIgnoreCase)) + { + return cert; + } + } + else + { + var store = new X509Store("temp"); + + store.Open(OpenFlags.ReadWrite); + store.Add(new X509Certificate2(currentCertContext)); + + foreach (var certificate in store.Certificates) + { + if (string.Equals(certificate.Thumbprint, thumbPrint, StringComparison.InvariantCultureIgnoreCase)) + { + return certificate; + } + } + } + } while (currentCertContext != (IntPtr)0); + } + + return null; + } + finally + { + if (safeHandle != IntPtr.Zero) + { + SafeNativeMethods.CertCloseStore(safeHandle, 0); + } + } + } + + /// + /// Gets a Remote Certificate Store + /// + /// + /// + /// + /// + /// An Integer Pointer (IntPtr) + /// + /// + // ReSharper disable once RedundantAssignment + private static IntPtr GetRemoteCertificateStore(StoreName storeName, StoreLocation location, string machineName, IntPtr safeHandle) + { + if (location == StoreLocation.CurrentUser) + { + safeHandle = SafeNativeMethods.CertOpenStore(CertStoreProvSystem, 0, 0, + CertSystemStoreCurrentUser, $@"\\{machineName}\{storeName}"); + } + else + { + safeHandle = SafeNativeMethods.CertOpenStore(CertStoreProvSystem, 0, 0, + CertSystemStoreLocalMachine, $@"\\{machineName}\{storeName}"); + } + + return safeHandle; + } + + /// + /// Finds the Keyfile Location on Disk + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static string FindKeyLocation(string keyFileName) + { + var commonAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + var machineKeyPath = commonAppDataPath + @"\Microsoft\Crypto\RSA\MachineKeys"; + + var fileList = Directory.EnumerateFiles(machineKeyPath, keyFileName); + if (fileList.Any()) + { + return machineKeyPath; + } + + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var rsaKeyPath = appDataPath + @"\Microsoft\Crypto\RSA\"; + + var directoryArray = Directory.GetDirectories(rsaKeyPath); + if (directoryArray.Length > 0) + { + foreach (var directory in directoryArray) + { + var matchingDirectories = Directory.EnumerateFiles(directory, keyFileName); + if (matchingDirectories.Any()) + { + return directory; + } + } + } + + var cryptoKeyPath = appDataPath + @"\Microsoft\Crypto\Keys\"; + var keyPath = Path.Combine(cryptoKeyPath, keyFileName); + if(File.Exists(keyPath)) + { + return cryptoKeyPath; + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/Exceptions/CertificateExceptionBase.cs b/Modules/Alkami.Ops.Common/Exceptions/CertificateExceptionBase.cs new file mode 100644 index 0000000..a875d04 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Exceptions/CertificateExceptionBase.cs @@ -0,0 +1,27 @@ +using System; + +namespace Alkami.Ops.Common.Exceptions +{ + public abstract class CertificateExceptionBase : Exception + { + private readonly string _certificateName; + private readonly string _certificateThumbPrint; + + public string CertificateName + { + get { return _certificateName; } + } + + public string CertificateThumbPrint + { + get { return _certificateThumbPrint; } + } + + protected CertificateExceptionBase(string message, string name, string thumbprint) + : base(message) + { + _certificateName = name; + _certificateThumbPrint = thumbprint; + } + } +} diff --git a/Modules/Alkami.Ops.Common/Exceptions/CertificateExportException.cs b/Modules/Alkami.Ops.Common/Exceptions/CertificateExportException.cs new file mode 100644 index 0000000..af3447d --- /dev/null +++ b/Modules/Alkami.Ops.Common/Exceptions/CertificateExportException.cs @@ -0,0 +1,25 @@ +namespace Alkami.Ops.Common.Exceptions +{ + public class CertificateExportException : CertificateExceptionBase + { + private readonly string _baseExceptionMessage; + private readonly string _subject; + + public string BaseExceptionMessage + { + get { return _baseExceptionMessage; } + } + + public string Subject + { + get { return _subject; } + } + + public CertificateExportException(string message, string name, string subject, string thumbprint, string baseExceptionMessage) + : base(message, name, thumbprint) + { + _baseExceptionMessage = baseExceptionMessage; + _subject = subject; + } + } +} diff --git a/Modules/Alkami.Ops.Common/Exceptions/InvalidCertificateException.cs b/Modules/Alkami.Ops.Common/Exceptions/InvalidCertificateException.cs new file mode 100644 index 0000000..0d20a72 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Exceptions/InvalidCertificateException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Alkami.Ops.Common.Exceptions +{ + public class InvalidCertificateException : CertificateExceptionBase + { + private readonly DateTime? _effectiveDateTime; + private readonly DateTime? _expirationDateTime; + + public DateTime? EffectiveDateTime + { + get { return _effectiveDateTime; } + } + + public DateTime? ExpirationDateTime + { + get { return _expirationDateTime; } + } + + public InvalidCertificateException(string message, string name, string thumbprint, DateTime? effectiveDate = null, DateTime? expirationDate = null) + : base(message, name, thumbprint) + { + _effectiveDateTime = effectiveDate; + _expirationDateTime = expirationDate; + } + } +} diff --git a/Modules/Alkami.Ops.Common/Exceptions/PrivateKeyNotFoundException.cs b/Modules/Alkami.Ops.Common/Exceptions/PrivateKeyNotFoundException.cs new file mode 100644 index 0000000..812b341 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Exceptions/PrivateKeyNotFoundException.cs @@ -0,0 +1,25 @@ +namespace Alkami.Ops.Common.Exceptions +{ + public class PrivateKeyNotFoundException : CertificateExceptionBase + { + private readonly string _storeName; + private readonly string _storeLocation; + + public string StoreName + { + get { return _storeName; } + } + + public string StoreLocation + { + get { return _storeLocation; } + } + + public PrivateKeyNotFoundException(string message, string name, string thumbprint, string storeName, string storeLocation) + : base(message, name, thumbprint) + { + _storeName = storeName; + _storeLocation = storeLocation; + } + } +} diff --git a/Modules/Alkami.Ops.Common/Extensions/StringExtensions.cs b/Modules/Alkami.Ops.Common/Extensions/StringExtensions.cs new file mode 100644 index 0000000..d806fa0 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Extensions/StringExtensions.cs @@ -0,0 +1,53 @@ +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Alkami.Ops.Common.Extensions +{ + public static class StringExtensions + { + private static readonly Regex Base64Regex = new Regex( + @"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Determines if a string is base 64 + /// + /// + /// + public static bool IsBase64String(this string s) + { + if (string.IsNullOrEmpty(s)) + { + return false; + } + + return (Base64Regex.IsMatch(s) && s.Length % 4 == 0); + } + + + /// + /// Removes characters that are invalid for a filename from a string + /// + /// + /// + public static string GetSanitizedFileName(this string fileName) + { + var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars())); + var invalidReStr = string.Format(@"[{0}]+", invalidChars); + + var reservedWords = new[] + { + "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", + "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", + "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + }; + + var sanitisedNamePart = Regex.Replace(fileName, invalidReStr, "_"); + + return reservedWords.Select(reservedWord => string.Format("^{0}\\.", reservedWord)) + .Aggregate(sanitisedNamePart, + (current, reservedWordPattern) => Regex.Replace(current, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase)); + } + } +} diff --git a/Modules/Alkami.Ops.Common/FileSystem/ZipHelpers.cs b/Modules/Alkami.Ops.Common/FileSystem/ZipHelpers.cs new file mode 100644 index 0000000..a97b737 --- /dev/null +++ b/Modules/Alkami.Ops.Common/FileSystem/ZipHelpers.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace Alkami.Ops.Common.FileSystem +{ + public static class ZipHelpers + { + /// + /// Unzip a path into a specific folder + /// + /// Path of the file to unzip. + /// If null defaults to a new folder at current location with the Zip file's name. + public static void UnZip(string zipPath, string destinationDirectory = null) + { + if (!string.IsNullOrWhiteSpace(zipPath)) + { + if (string.IsNullOrWhiteSpace(destinationDirectory)) + { + destinationDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + destinationDirectory = Path.Combine(destinationDirectory, Path.GetFileNameWithoutExtension(zipPath)); + } + + Directory.CreateDirectory(destinationDirectory); + + try + { + using (var archive = ZipFile.OpenRead(zipPath)) + { + archive.ExtractToDirectory(destinationDirectory); + } + } + catch (IOException ex) + { + Console.Error.WriteLine("Exception occurred while extracting files."); + + Console.Error.WriteLine(ex); + } + } + } + + /// + /// Unzips a single directory from inside a zip file. + /// + /// Path of the file to unzip. + /// Directory inside of zip file to be extracted. If null or nonexistent nothing will be extracted. + /// If null defaults to a new folder at current location with the Zip subfolder's name. + public static void UnZipSubDirectory(string internalDirectory, string zipPath, string destinationDirectory = null) + { + if (!string.IsNullOrWhiteSpace(zipPath)) + { + if (string.IsNullOrWhiteSpace(destinationDirectory)) + { + destinationDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + destinationDirectory = Path.Combine(destinationDirectory, Path.GetFileNameWithoutExtension(zipPath)); + } + + Directory.CreateDirectory(destinationDirectory); + + try + { + using (var archive = ZipFile.OpenRead(zipPath)) + { + foreach (var entry in archive.Entries) + { + if (GetRootFolder(entry.FullName).IndexOf(internalDirectory, StringComparison.OrdinalIgnoreCase) >= 0) + { + ExtractEntry(entry, destinationDirectory, entry.FullName); + } + } + } + } + catch (IOException ex) + { + Console.Error.WriteLine("Exception occurred while extracting files."); + + Console.Error.WriteLine(ex); + } + catch (Exception ex) + { + Console.Error.WriteLine("Unknown exception occurred while extracting files"); + + Console.Error.WriteLine(ex); + + throw; + } + } + } + + /// + /// Recursively handle subfolders + /// + /// Zip entry to extract + /// Desired destination directory. Is added to with each recursive loop as subdirectories are popped off of the entry + /// Current entry path. Shorter every loop as we pop off directories and add them to the destinationDirectory parameter. + private static void ExtractEntry(ZipArchiveEntry entry, string destinationDirectory, string entryPath) + { + var targetIsDirectory = false; + var topLevelDirectory = ""; + + if (!string.IsNullOrWhiteSpace(entryPath) && !entryPath.Equals('/')) + { + topLevelDirectory = Path.GetDirectoryName(entryPath); + } + else + { + targetIsDirectory = true; + } + + if (!string.IsNullOrEmpty(topLevelDirectory)) + { + Directory.CreateDirectory(Path.Combine(destinationDirectory, topLevelDirectory)); + destinationDirectory = Path.Combine(destinationDirectory, topLevelDirectory); + ExtractEntry(entry, destinationDirectory, Path.GetFileName(entryPath)); + } + else + { + if (targetIsDirectory) + { + Directory.CreateDirectory(destinationDirectory); + } + else + { + entry.ExtractToFile(Path.Combine(destinationDirectory, entry.Name)); + } + } + } + + /// + /// Cargo culted from https://stackoverflow.com/a/40124633/3691973 + /// + /// Path to examine + /// Top level folder. + private static string GetRootFolder(string path) + { + var root = Path.GetPathRoot(path); + while (true) + { + var temp = Path.GetDirectoryName(path); + if (temp != null && temp.Equals(root)) + break; + path = temp; + } + return path; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/NativeMethods/SafeNativeMethods.cs b/Modules/Alkami.Ops.Common/NativeMethods/SafeNativeMethods.cs new file mode 100644 index 0000000..0a25aa5 --- /dev/null +++ b/Modules/Alkami.Ops.Common/NativeMethods/SafeNativeMethods.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; + +namespace Alkami.Ops.Common.NativeMethods +{ + internal static class SafeNativeMethods + { + #region CRYPT32 - Certificates and Store Management + + /// + /// This struct is used when working with PFX files + /// + [StructLayout(LayoutKind.Sequential)] + internal struct CRYPT_DATA_BLOB + { + public int cbData; + public IntPtr pbData; + } + + [DllImport("CRYPT32", SetLastError = true)] + internal static extern Boolean PFXExportCertStoreEx( + IntPtr hCertStore, + ref CRYPT_DATA_BLOB pPFX, + [MarshalAs(UnmanagedType.LPWStr)] String szPassword, + IntPtr pvReserved, + uint dwFlags + ); + + internal const uint EXPORT_PRIVATE_KEYS = 0x0004; + + [DllImport("CRYPT32")] + internal static extern bool PFXIsPFXBlob(ref CRYPT_DATA_BLOB pPfx); + + [DllImport("CRYPT32", SetLastError = true)] + internal static extern Boolean CertAddCertificateContextToStore(IntPtr hCertStore, IntPtr pCertContext, Int32 dwAddDisposition, ref IntPtr ppStoreContext); + + [DllImport("CRYPT32", EntryPoint = "CertAddEncodedCertificateToStore", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CertAddEncodedCertificateToStore(IntPtr certStore, int certEncodingType, byte[] certEncoded, int certEncodedLength, int addDisposition, IntPtr certContext); + + [DllImport("CRYPT32", EntryPoint = "CertCloseStore", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool CertCloseStore(IntPtr storeProvider, int flags); + + [DllImport("CRYPT32", EntryPoint = "CertEnumCertificatesInStore", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CertEnumCertificatesInStore(IntPtr storeProvider, IntPtr prevCertContext); + + [DllImport("CRYPT32", EntryPoint = "CertOpenStore", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CertOpenStore(int storeProvider, int encodingType, int hcryptProv, int flags, string pvPara); + + [DllImport("CRYPT32", SetLastError = true)] + internal static extern IntPtr PFXImportCertStore(ref CRYPT_DATA_BLOB pPfx, [MarshalAs(UnmanagedType.LPWStr)] String szPassword, uint dwFlags); + + #endregion CRYPT32 - Certificates and Store Management + } +} diff --git a/Modules/Alkami.Ops.Common/Properties/AssemblyInfo.cs b/Modules/Alkami.Ops.Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c727968 --- /dev/null +++ b/Modules/Alkami.Ops.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Alkami.Ops.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Alkami.Ops.Common")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: AssemblyVersion("4.0.0.0")] +[assembly: AssemblyFileVersion("4.0.0.0")] +[assembly: AssemblyInformationalVersion("FFFFFFF")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("74df4cfb-95a5-43a3-8f61-8cac705c820d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/app.config b/Modules/Alkami.Ops.Common/app.config new file mode 100644 index 0000000..64ecd71 --- /dev/null +++ b/Modules/Alkami.Ops.Common/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.Ops.Common/tools/chocolateyInstall.ps1 b/Modules/Alkami.Ops.Common/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.Ops.Common/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.Common/tools/chocolateyUninstall.ps1 b/Modules/Alkami.Ops.Common/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.Ops.Common/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.Ops.SecretServer.Console/Alkami.Ops.SecretServer.Console.csproj b/Modules/Alkami.Ops.SecretServer.Console/Alkami.Ops.SecretServer.Console.csproj new file mode 100644 index 0000000..e608a22 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer.Console/Alkami.Ops.SecretServer.Console.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {9479CEB4-0D31-41F3-A38E-618CEDC058F2} + Exe + Properties + Alkami.Ops.SecretServer.Console + Alkami.Ops.SecretServer.Console + v4.6.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + {c21a005e-6a04-46f8-8dd7-e777eeb243e3} + Alkami.Ops.SecretServer + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer.Console/App.config b/Modules/Alkami.Ops.SecretServer.Console/App.config new file mode 100644 index 0000000..0e8d93b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer.Console/App.config @@ -0,0 +1,34 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer.Console/Program.cs b/Modules/Alkami.Ops.SecretServer.Console/Program.cs new file mode 100644 index 0000000..c824574 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer.Console/Program.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using Alkami.Ops.SecretServer.Model; + +namespace Alkami.Ops.SecretServer.Console +{ + class Program + { + /// + /// Simple example console app to pull all secrets from a given folder + /// + /// + static void Main(string[] args) + { + try + { + using (var client = new Client()) + { + var result = client.AuthenticateAsync("ops.deployment", @"PasswordGoesHere!", "corp.alkamitech.com").Result; + var secretSearchResults = client.GetFolderSecretsAsync("POD6").Result; + + foreach (var cert in secretSearchResults.Secrets.Where(s => s.Value is Certificate).Select(s => s.Value)) + { + var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + var downloadPath = ((Certificate)cert).SaveFileToDisk(path); + System.Console.WriteLine($"Downloaded certificate to {downloadPath}"); + } + + foreach (var user in secretSearchResults.Secrets.Where(s => s.Value is User).Select(s => s.Value)) + { + var castUser = (User)user; + System.Console.WriteLine($"Retrieved user {castUser.UserName} with password {castUser.Password}"); + } + } + } + catch (Exception e) + { + System.Console.WriteLine(e); + throw; + } + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer.Console/Properties/AssemblyInfo.cs b/Modules/Alkami.Ops.SecretServer.Console/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d6f8044 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer.Console/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Alkami.Ops.SecretServer.Console")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Alkami.Ops.SecretServer.Console")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9479ceb4-0d31-41f3-a38e-618cedc058f2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Modules/Alkami.Ops.SecretServer.Console/packages.config b/Modules/Alkami.Ops.SecretServer.Console/packages.config new file mode 100644 index 0000000..6b8deb9 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer.Console/packages.config @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.csproj b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.csproj new file mode 100644 index 0000000..d239a97 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.csproj @@ -0,0 +1,264 @@ + + + + + Debug + AnyCPU + {C21A005E-6A04-46F8-8DD7-E777EEB243E3} + Library + Properties + Alkami.Ops.SecretServer + Alkami.Ops.SecretServer + v4.6.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + OnOutputUpdated + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Reference.svcmap + + + + + + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + Reference.svcmap + + + + + + + + + + + + + + + + + + + + + + + WCF Proxy Generator + Reference.cs + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.nuspec b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.nuspec new file mode 100644 index 0000000..f0714bb --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.nuspec @@ -0,0 +1,19 @@ + + + + Alkami.Ops.SecretServer + $version$ + Alkami Technology + Alkami Technology + false + SecretServer Client API + + + + + + + + + + diff --git a/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.psd1 b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.psd1 new file mode 100644 index 0000000..02a352d --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Alkami.Ops.SecretServer.psd1 @@ -0,0 +1,18 @@ +@{ + RootModule = 'Alkami.Ops.SecretServer.psm1' + ModuleVersion = '3.2.2' + GUID = '5e584ed1-1a5a-4de6-84a7-4fe5b54ce552' + Author = 'SRE,dsage,cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A Simple Client for Secret Server' + PowerShellVersion = '5.0' + PrivateData = @{ + PSData = @{ + Tags = @('Secret Server') + ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.Ops.SecretServer' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.SecretServer' +} diff --git a/Modules/Alkami.Ops.SecretServer/Client.cs b/Modules/Alkami.Ops.SecretServer/Client.cs new file mode 100644 index 0000000..655de32 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Client.cs @@ -0,0 +1,303 @@ +using System; +using System.Linq; +using System.ServiceModel; +using System.Threading.Tasks; +using Alkami.Ops.SecretServer.Enum; +using Alkami.Ops.SecretServer.Messages; +using Alkami.Ops.SecretServer.Model; +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer +{ + /// + /// A simple client for secret server + /// + public sealed class Client : IDisposable + { + private string _authenticationToken; + + private readonly SSWebServiceSoapClient _client; + + /// + /// Gets all secrets from a folder based on folder name + /// + /// + /// + public async Task GetFolderSecretsAsync(string folderName) + { + try + { + Console.WriteLine($"Attempting to find all secrets in folder: {folderName}"); + var folderSearchResult = await FindFolderAsync(folderName); + + if (folderSearchResult.Status == ResultStatus.Success) + { + Console.WriteLine($"Attempting to get all secrets from folder with name: {folderName} and folder ID: {folderSearchResult.Folders.First().Id}"); + return await GetAllSecretsFromFolderAsync(folderSearchResult.Folders.First()); + } + + Console.WriteLine($"Attempt to find folder {folderName} resulted in errors: {string.Join(",", folderSearchResult.Errors)}"); + return new SecretSearchResult + { + Errors = folderSearchResult.Errors, + Message = folderSearchResult.Message + }; + } + catch (Exception e) + { + Console.WriteLine($"An error occurred while attempting to get all secrets from folder with name: {folderName}. Exception: {e}"); + throw; + } + } + + /// + /// Authenticates using the supplied username, password, and domain + /// + /// + /// + /// + /// + public async Task AuthenticateAsync(string userName, string password, string domain) + { + try + { + Console.WriteLine($"Attempting authentication with username: {userName} and domain {domain}"); + var result = await _client.AuthenticateAsync(userName, password, string.Empty, domain); + + Console.WriteLine(result.Errors != null && result.Errors.Any() + ? $"Encountered errors during authentication: {string.Join(",", result.Errors)}" + : "Authentication successful"); + + return new AuthenticationResult + { + Status = (result.Errors != null && result.Errors.Any() ? ResultStatus.Failure : ResultStatus.Success), + Message = (result.Errors != null && result.Errors.Any() ? "Authentication Failed" : "Authentication Successful"), + Errors = result.Errors?.Select(e => new ResultError(null, e)).ToList(), + Token = _authenticationToken = result.Token + }; + } + catch (Exception e) + { + Console.WriteLine($"An error occurred while attempting to authentice with username: {userName} and domain {domain}. Exception: {e}"); + throw; + } + } + + /// + /// Returns a collection of secrets from a folder as a + /// + /// + /// + private async Task GetAllSecretsFromFolderAsync(Folder folder) + { + try + { + Console.WriteLine($"Attempting to pull all secrets from folder {folder.Name}"); + var folderSearchResponse = await _client.SearchSecretsByFolderAsync(_authenticationToken, "*", folder.Id, false, false, true); + var results = new SecretSearchResult(); + + if (folderSearchResponse.SearchSecretsByFolderResult.Errors != null && folderSearchResponse.SearchSecretsByFolderResult.Errors.Any()) + { + Console.WriteLine($"Attempt to pull secrets from folder {folder.Name} resulted in errors: {string.Join(",", folderSearchResponse.SearchSecretsByFolderResult.Errors)}"); + results.Errors = folderSearchResponse.SearchSecretsByFolderResult.Errors.Select(e => new ResultError(null, e)).ToList(); + results.Status = ResultStatus.Failure; + } + + foreach (var secretSummary in folderSearchResponse.SearchSecretsByFolderResult.SecretSummaries) + { + Console.WriteLine($"Getting secret details for secret ID: {secretSummary.SecretId} with name: {secretSummary.SecretName}"); + var secretResponse = await _client.GetSecretAsync(_authenticationToken, secretSummary.SecretId, false, null); + + if (secretResponse.GetSecretResult.Errors.Any()) + { + Console.WriteLine($"Attempt to pull secrets from folder {folder.Name} resulted in errors: {string.Join(",", secretResponse.GetSecretResult.Errors)}"); + results.Errors.Add(new ResultError(secretSummary.SecretId, + $"Error {secretResponse.GetSecretResult.Errors[0]} occurred while getting secret {secretSummary.SecretId}")); + results.Status = ResultStatus.PartialFailure; + continue; + } + + switch (secretSummary.SecretTypeName.ToUpperInvariant()) + { + case "SSL CERTIFICATE": + case "CERTIFICATE": + { + Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type Certificate"); + var certificate = new Certificate(secretResponse.GetSecretResult.Secret); + + if (!certificate.FileId.HasValue) + { + Console.WriteLine($"Certificate with secret ID {secretSummary.SecretId} does not have an attachment"); + results.Errors.Add(new ResultError(secretSummary.SecretId, + $"Unable to find a certificate attachemnt for secret {certificate.Id}")); + continue; + } + + // Download the PFX Files + Console.WriteLine($"Attempting to download attachment for Certificate with secret ID {secretSummary.SecretId}"); + var certificateAttachment = await DownloadFileAsync(certificate.Id, certificate.FileId.Value); + + certificate.FileAttachment = certificateAttachment.FileAttachment; + + certificate.FileName = certificateAttachment.FileName; + results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, certificate); + break; + } + case "WINDOWS ACCOUNT": + { + Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type User"); + results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, new Model.User(secretResponse.GetSecretResult.Secret)); + break; + } + case "SQL SERVER ACCOUNT": + { + Console.WriteLine($"Secret with ID {secretSummary.SecretId} determined to be of type ConnectionString"); + results.Secrets.Add(secretResponse.GetSecretResult.Secret.Id, new ConnectionString(secretResponse.GetSecretResult.Secret)); + break; + } + default: + { + Console.WriteLine($"Secret with ID {secretSummary.SecretId} is an unmapped type"); + results.Errors.Add(new ResultError(secretSummary.SecretId, + $"Unknown secret type {secretSummary.SecretTypeName} found for secret ID {secretSummary.SecretId}")); + results.Status = ResultStatus.PartialFailure; + break; + } + } + } + + if (!results.Secrets.Any()) + { + // All secret searches failed + results.Status = ResultStatus.Failure; + } + else if (results.Status == ResultStatus.Unknown) + { + // All secret searches passed, otherwise this would be PartialFailure + results.Status = ResultStatus.Success; + } + + return results; + } + catch (Exception e) + { + Console.WriteLine($"An error occurred while attempting to pull all secrets from folder {folder.Name}. Exception: {e}"); + throw; + } + } + + /// + /// Downloads a file based on the parent Secret and File ItemId + /// + /// + /// + /// + private async Task DownloadFileAsync(int secretId, int fileItemId) + { + try + { + Console.WriteLine($"Attempting to download file attachment for secret with ID: {secretId} and fileItemID: {fileItemId}"); + var downloadResponse = await _client.DownloadFileAttachmentByItemIdAsync(_authenticationToken, secretId, fileItemId); + var result = new Messages.FileDownloadResult(); + + if (downloadResponse.Errors.Any() || downloadResponse.FileAttachment.Length == 0) + { + Console.WriteLine($"Attempt to download attachment from secret with ID {secretId} resulted in errors: {string.Join(",", downloadResponse.Errors)}"); + result.Status = ResultStatus.Failure; + result.Errors = downloadResponse.Errors.Select(e => new ResultError(secretId, e)).ToList(); + } + else + { + result.Status = ResultStatus.Success; + result.FileAttachment = downloadResponse.FileAttachment; + result.FileName = downloadResponse.FileName; + } + + return result; + } + catch (Exception e) + { + Console.WriteLine($"An error occurred while attempting to download file attachment for secret with ID: {secretId} and fileItemID: {fileItemId}. Exception: {e}"); + throw; + } + } + + /// + /// Finds a Secret Folder based on FolderName + /// + /// + /// + private async Task FindFolderAsync(string folderName) + { + try + { + Console.WriteLine($"Attempting to find folder with name: {folderName}"); + var folderResponse = await _client.SearchFoldersAsync(_authenticationToken, folderName); + + if (!folderResponse.Errors.Any() && folderResponse.Folders.Count() == 1) + { + return new FolderSearchResult + { + Status = ResultStatus.Success, + Folders = folderResponse.Folders + }; + } + + if (folderResponse.Errors.Any()) + { + Console.WriteLine($"Attempt to find folder with name {folderName} resulted in errors: {string.Join(",", folderResponse.Errors)}"); + } + else if (folderResponse.Folders.Count() > 1) + { + Console.WriteLine("More than one folder was returned -- narrow your search and/or check the secret folder configuration."); + } + + return new FolderSearchResult + { + Errors = folderResponse.Errors.Select(e => new ResultError(null, e)).ToList(), + Status = ResultStatus.Failure, + Message = $"Folder Search for {folderName} Failed" + }; + } + catch (Exception e) + { + Console.WriteLine($"An error occurred while attempting to find folder with name: {folderName}. Exception: {e}"); + throw; + } + } + + /// + /// Instantiates a new instance of the Client class + /// + public Client() + { + const string endpointAddress = "https://secret.corp.alkamitech.com/webservices/SSWebService.asmx"; + var binding = new BasicHttpBinding + { + Name = "SSWebServiceSoap", + Security = + { + Mode = BasicHttpSecurityMode.Transport + } + }; + + var endpoint = new EndpointAddress(endpointAddress); + _client = new SSWebServiceSoapClient(binding, endpoint); + } + + #region Implement IDisposable + + /// + /// Implementation of IDisposable. + /// + /// + /// Calls Dispose on the + /// + public void Dispose() + { + ((IDisposable)_client)?.Dispose(); + } + + #endregion Implement IDisposable + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Enum/ResultStatus.cs b/Modules/Alkami.Ops.SecretServer/Enum/ResultStatus.cs new file mode 100644 index 0000000..7cf115e --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Enum/ResultStatus.cs @@ -0,0 +1,13 @@ +namespace Alkami.Ops.SecretServer.Enum +{ + /// + /// An Enum which represents the overall status of the request + /// + public enum ResultStatus + { + Unknown = 0, + Success = 1, + Failure = 2, + PartialFailure = 3 + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Enum/SecretType.cs b/Modules/Alkami.Ops.SecretServer/Enum/SecretType.cs new file mode 100644 index 0000000..034bebc --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Enum/SecretType.cs @@ -0,0 +1,13 @@ +namespace Alkami.Ops.SecretServer.Enum +{ + /// + /// An enum representing the type of the secret pulled from the server + /// + public enum SecretType + { + Unknown, + Certificate, + User, + ConnectionString + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Messages/AuthenticationResult.cs b/Modules/Alkami.Ops.SecretServer/Messages/AuthenticationResult.cs new file mode 100644 index 0000000..b07fa30 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/AuthenticationResult.cs @@ -0,0 +1,13 @@ +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Result Message from an Authentication Request + /// + public class AuthenticationResult : MessageBase + { + /// + /// The authentication token generated during login + /// + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Messages/FileDownloadResult.cs b/Modules/Alkami.Ops.SecretServer/Messages/FileDownloadResult.cs new file mode 100644 index 0000000..be6047c --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/FileDownloadResult.cs @@ -0,0 +1,19 @@ + +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Result Message from File Download request + /// + public class FileDownloadResult : MessageBase + { + /// + /// The file attachment as a byte array + /// + public byte[] FileAttachment { get; set; } + + /// + /// The file name + /// + public string FileName { get; set; } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Messages/FolderSearchResult.cs b/Modules/Alkami.Ops.SecretServer/Messages/FolderSearchResult.cs new file mode 100644 index 0000000..d768ba9 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/FolderSearchResult.cs @@ -0,0 +1,15 @@ +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Result message from a folder search request + /// + public class FolderSearchResult : MessageBase + { + /// + /// The collection of folders returned from the request + /// + public Folder[] Folders { get; set; } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Messages/MessageBase.cs b/Modules/Alkami.Ops.SecretServer/Messages/MessageBase.cs new file mode 100644 index 0000000..68a923f --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/MessageBase.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Alkami.Ops.SecretServer.Enum; + +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Base class for all messages returned from various requests + /// + public class MessageBase + { + /// + /// An informational message about the request + /// + public string Message { get; set; } + + /// + /// The of the request + /// + public ResultStatus Status { get; set; } + + /// + /// Any errors returned during the request + /// + public List Errors { get; set; } = new List(); + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Messages/ResultError.cs b/Modules/Alkami.Ops.SecretServer/Messages/ResultError.cs new file mode 100644 index 0000000..21289df --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/ResultError.cs @@ -0,0 +1,33 @@ +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Represents an error returned during a request + /// + public class ResultError + { + /// + /// The secret ID if applicable + /// + public int? SecretId { get; private set; } + + /// + /// The error message added by the client or returned from SecretServer + /// + public string ErrorMessage { get; private set; } + + /// + /// Instantiates a new instance of the ResultError class + /// + /// + /// + public ResultError(int? secretId, string errorMessage) + { + if (secretId.HasValue) + { + SecretId = secretId.Value; + } + + ErrorMessage = errorMessage; + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Messages/SecretSearchResult.cs b/Modules/Alkami.Ops.SecretServer/Messages/SecretSearchResult.cs new file mode 100644 index 0000000..f5ef4ee --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Messages/SecretSearchResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Alkami.Ops.SecretServer.Model; + +namespace Alkami.Ops.SecretServer.Messages +{ + /// + /// Result message from a secret search request + /// + public class SecretSearchResult : MessageBase + { + /// + /// The collection of secrets returned from the request + /// + public Dictionary Secrets { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Model/Certificate.cs b/Modules/Alkami.Ops.SecretServer/Model/Certificate.cs new file mode 100644 index 0000000..364b5b9 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Model/Certificate.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Linq; +using Alkami.Ops.SecretServer.Enum; +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer.Model +{ + /// + /// Represents a Certificate or SSL Certificate secret + /// + public class Certificate : SecretBase + { + /// + /// The Certificate name from the "Certificate Name" field on the secret + /// + public string Name => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Certificate Name", StringComparison.OrdinalIgnoreCase))?.Value; + + /// + /// The Certificate password from the "Import Password" field on the secret + /// + public string Password => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Import Password", StringComparison.OrdinalIgnoreCase) && p.IsPassword)?.Value; + + /// + /// The FileID of the attachment on the secret from the "Pfx File" field + /// + public int? FileId => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Pfx File", StringComparison.OrdinalIgnoreCase) && p.IsFile)?.Id; + + /// + /// The secret type + /// + /// + /// Returns .Certificate + /// + public new SecretType SecretType => SecretType.Certificate; + + /// + /// The FileName of the attachment. Added externally from a subsequent call to SecretServer + /// + public string FileName { get; set; } + + /// + /// The FileAttachment as a byte array. Added externally from a subsequent call to SecretServer + /// + public byte[] FileAttachment { get; set; } + + /// + /// Saves the to disk + /// + /// The path to save the file to. The FileName is appended from + /// + public string SaveFileToDisk(string outputPath) + { + var outFile = Path.Combine(outputPath, FileName); + File.WriteAllBytes(outFile, FileAttachment); + + return outFile; + } + + /// + /// Instantiates a new instance of the Certificate class + /// + /// + public Certificate(Secret secret) : base(secret) + { + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Model/ConnectionString.cs b/Modules/Alkami.Ops.SecretServer/Model/ConnectionString.cs new file mode 100644 index 0000000..1e52236 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Model/ConnectionString.cs @@ -0,0 +1,43 @@ +using System; +using System.Data.SqlClient; +using System.Linq; +using Alkami.Ops.SecretServer.Enum; +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer.Model +{ + /// + /// Represents a ConnectionString secret + /// + /// + /// This is, for now, being built out of a 'Sql Server Account' template in SecretServer + /// + public class ConnectionString : SecretBase + { + /// + /// The secret type + /// + /// + /// Returns .ConnectionString + /// + public new SecretType SecretType => SecretType.ConnectionString; + + /// + /// The raw connection string pulled from the secret's Username field + /// + public string RawConnectionString => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Username", StringComparison.OrdinalIgnoreCase))?.Value; + + /// + /// A object built from + /// + public SqlConnectionStringBuilder ConnectionStringBuilder => !string.IsNullOrEmpty(RawConnectionString) ? new SqlConnectionStringBuilder(RawConnectionString) : null; + + /// + /// Instantiates a new instance of the Certificate class + /// + /// + public ConnectionString(Secret secret) : base(secret) + { + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Model/SecretBase.cs b/Modules/Alkami.Ops.SecretServer/Model/SecretBase.cs new file mode 100644 index 0000000..8faf4e0 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Model/SecretBase.cs @@ -0,0 +1,55 @@ +using Alkami.Ops.SecretServer.Enum; +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer.Model +{ + public class SecretBase + { + /// + /// The Secret Object + /// + private readonly Secret _secret; + + /// + /// The collection of items attached to the secret + /// + internal SecretItem[] SecretItems => _secret.Items; + + /// + /// The secret type + /// + /// + /// Returns .Unknown + /// + public SecretType SecretType => SecretType.Unknown; + + /// + /// The ID of the _secret + /// + public int Id => _secret.Id; + + /// + /// The name of the secret + /// + public string SecretName => _secret.Name; + + /// + /// The parent folder ID + /// + public int FolderId => _secret.FolderId; + + /// + /// The SecretServer secret type ID + /// + public int SecretTypeId => _secret.SecretTypeId; + + /// + /// Base class constructor + /// + /// + public SecretBase(Secret secret) + { + _secret = secret; + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Model/User.cs b/Modules/Alkami.Ops.SecretServer/Model/User.cs new file mode 100644 index 0000000..79d9395 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Model/User.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Alkami.Ops.SecretServer.Enum; +using Alkami.Ops.SecretServer.SSWebService; + +namespace Alkami.Ops.SecretServer.Model +{ + /// + /// Represents a Windows Account secret + /// + public class User : SecretBase + { + /// + /// The secret type + /// + /// + /// Returns .Certificate + /// + public new SecretType SecretType => SecretType.User; + + /// + /// The user name from the "Username" field on the secret + /// + public string UserName => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Username", StringComparison.OrdinalIgnoreCase))?.Value; + + /// + /// The password from the "Password" field on the secret + /// + public string Password => SecretItems.FirstOrDefault(p => string.Equals(p.FieldName, "Password", StringComparison.OrdinalIgnoreCase) && p.IsPassword)?.Value; + + /// + /// Instantiates a new instance of the Secret class + /// + /// + public User(Secret secret) : base(secret) + { + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Properties/AssemblyInfo.cs b/Modules/Alkami.Ops.SecretServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..eee3ed6 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Alkami.Ops.SecretServer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Alkami.Ops.SecretServer")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c21a005e-6a04-46f8-8dd7-e777eeb243e3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretCustomAuditResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretCustomAuditResponse.datasource new file mode 100644 index 0000000..0bc6c4b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretCustomAuditResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretResult.datasource new file mode 100644 index 0000000..2719b6d --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AddSecretResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.AddSecretResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AuthenticateResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AuthenticateResult.datasource new file mode 100644 index 0000000..1c943fd --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.AuthenticateResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.AuthenticateResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.CreateFolderResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.CreateFolderResult.datasource new file mode 100644 index 0000000..b689b19 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.CreateFolderResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.CreateFolderResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FileDownloadResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FileDownloadResult.datasource new file mode 100644 index 0000000..65cdb1a --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FileDownloadResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.FileDownloadResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedCreateResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedCreateResult.datasource new file mode 100644 index 0000000..6fe30e8 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedCreateResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.FolderExtendedCreateResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetNewResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetNewResult.datasource new file mode 100644 index 0000000..f240910 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetNewResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetResult.datasource new file mode 100644 index 0000000..ca1e57b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedGetResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedUpdateResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedUpdateResult.datasource new file mode 100644 index 0000000..a0bf600 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.FolderExtendedUpdateResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.FolderExtendedUpdateResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GeneratePasswordResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GeneratePasswordResult.datasource new file mode 100644 index 0000000..d472e1d --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GeneratePasswordResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GeneratePasswordResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllGroupsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllGroupsResult.datasource new file mode 100644 index 0000000..e6121d3 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllGroupsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetAllGroupsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllSSHCommandMenusResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllSSHCommandMenusResponse.datasource new file mode 100644 index 0000000..c45e1a2 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetAllSSHCommandMenusResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetCheckOutStatusResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetCheckOutStatusResult.datasource new file mode 100644 index 0000000..7aa5558 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetCheckOutStatusResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetCheckOutStatusResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependenciesResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependenciesResult.datasource new file mode 100644 index 0000000..ccd3d24 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependenciesResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetDependenciesResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependencyGroupsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependencyGroupsResult.datasource new file mode 100644 index 0000000..48080f4 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetDependencyGroupsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetDependencyGroupsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFavoritesResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFavoritesResult.datasource new file mode 100644 index 0000000..df8ecd5 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFavoritesResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetFavoritesResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFolderResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFolderResult.datasource new file mode 100644 index 0000000..227eba0 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFolderResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetFolderResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFoldersResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFoldersResult.datasource new file mode 100644 index 0000000..3760a33 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetFoldersResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetFoldersResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretAuditResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretAuditResult.datasource new file mode 100644 index 0000000..893c4b5 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretAuditResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretAuditResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResponse.datasource new file mode 100644 index 0000000..248c28e --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResult.datasource new file mode 100644 index 0000000..3f67f6e --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplateFieldsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplateFieldsResult.datasource new file mode 100644 index 0000000..d135eee --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplateFieldsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretTemplateFieldsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplatesResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplatesResult.datasource new file mode 100644 index 0000000..35bd093 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretTemplatesResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretTemplatesResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretsByFieldValueResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretsByFieldValueResult.datasource new file mode 100644 index 0000000..6dfd52a --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSecretsByFieldValueResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSecretsByFieldValueResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSitesResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSitesResult.datasource new file mode 100644 index 0000000..0f0fb41 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSitesResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSitesResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenuResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenuResult.datasource new file mode 100644 index 0000000..2f1e32b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenuResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenuResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenusResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenusResult.datasource new file mode 100644 index 0000000..feb18a7 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetSshCommandMenusResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenusResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetTicketSystemsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetTicketSystemsResult.datasource new file mode 100644 index 0000000..2ba2cc2 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetTicketSystemsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetTicketSystemsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserResult.datasource new file mode 100644 index 0000000..897447b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetUserResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptResult.datasource new file mode 100644 index 0000000..b458304 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetUserScriptResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptsResult.datasource new file mode 100644 index 0000000..1fa1074 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUserScriptsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetUserScriptsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUsersResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUsersResult.datasource new file mode 100644 index 0000000..debebaf --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.GetUsersResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.GetUsersResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.ImpersonateResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.ImpersonateResult.datasource new file mode 100644 index 0000000..37d5bdd --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.ImpersonateResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.ImpersonateResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.RequestApprovalResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.RequestApprovalResult.datasource new file mode 100644 index 0000000..713f77d --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.RequestApprovalResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.RequestApprovalResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SSHCredentialsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SSHCredentialsResult.datasource new file mode 100644 index 0000000..4ecfcf7 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SSHCredentialsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SSHCredentialsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchFolderResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchFolderResult.datasource new file mode 100644 index 0000000..90d4b18 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchFolderResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchFolderResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretPoliciesResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretPoliciesResult.datasource new file mode 100644 index 0000000..f121e32 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretPoliciesResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchSecretPoliciesResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse.datasource new file mode 100644 index 0000000..34c8069 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderResponse.datasource new file mode 100644 index 0000000..afcba03 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsByFolderResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResponse.datasource new file mode 100644 index 0000000..c813b0f --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResult.datasource new file mode 100644 index 0000000..039deba --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SearchSecretsResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretItemHistoryResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretItemHistoryResult.datasource new file mode 100644 index 0000000..bd8fd30 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretItemHistoryResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SecretItemHistoryResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyForSecretResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyForSecretResult.datasource new file mode 100644 index 0000000..b7f6591 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyForSecretResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecretResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyResult.datasource new file mode 100644 index 0000000..f8d5627 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SecretPolicyResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SecretPolicyResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SetCheckOutEnabledResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SetCheckOutEnabledResponse.datasource new file mode 100644 index 0000000..25a692e --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.SetCheckOutEnabledResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.TokenIsValidResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.TokenIsValidResult.datasource new file mode 100644 index 0000000..6e2381b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.TokenIsValidResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.TokenIsValidResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserResult.datasource new file mode 100644 index 0000000..271ef5a --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.UpdateUserResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserScriptResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserScriptResult.datasource new file mode 100644 index 0000000..c8a52ae --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UpdateUserScriptResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.UpdateUserScriptResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse.datasource new file mode 100644 index 0000000..e451220 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentResponse.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentResponse.datasource new file mode 100644 index 0000000..763a389 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UploadFileAttachmentResponse.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentResponse, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UserInfoResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UserInfoResult.datasource new file mode 100644 index 0000000..16bd4b1 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.UserInfoResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.UserInfoResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.VersionGetResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.VersionGetResult.datasource new file mode 100644 index 0000000..547ac91 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.VersionGetResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.VersionGetResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.WebServiceResult.datasource b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.WebServiceResult.datasource new file mode 100644 index 0000000..c065c8b --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Alkami.DevOps.SecretServer.SSWebService.WebServiceResult.datasource @@ -0,0 +1,10 @@ + + + + Alkami.Ops.SecretServer.SSWebService.WebServiceResult, Service References.SSWebService.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.cs b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.cs new file mode 100644 index 0000000..93b3d37 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.cs @@ -0,0 +1,9337 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Alkami.Ops.SecretServer.SSWebService { + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ServiceModel.ServiceContractAttribute(Namespace="urn:thesecretserver.com", ConfigurationName="SSWebService.SSWebServiceSoap")] + public interface SSWebServiceSoap { + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ApproveSecretAccessRequest", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.RequestApprovalResult ApproveSecretAccessRequest(string approvalId, string hours, bool userOverride); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ApproveSecretAccessRequest", ReplyAction="*")] + System.Threading.Tasks.Task ApproveSecretAccessRequestAsync(string approvalId, string hours, bool userOverride); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DenySecretAccessRequest", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.RequestApprovalResult DenySecretAccessRequest(string approvalId, bool userOverride); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DenySecretAccessRequest", ReplyAction="*")] + System.Threading.Tasks.Task DenySecretAccessRequestAsync(string approvalId, bool userOverride); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/Authenticate", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.AuthenticateResult Authenticate(string username, string password, string organization, string domain); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/Authenticate", ReplyAction="*")] + System.Threading.Tasks.Task AuthenticateAsync(string username, string password, string organization, string domain); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ImpersonateUser", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.ImpersonateResult ImpersonateUser(string token, string username, string organization, string domain); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ImpersonateUser", ReplyAction="*")] + System.Threading.Tasks.Task ImpersonateUserAsync(string token, string username, string organization, string domain); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AuthenticateRADIUS", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.AuthenticateResult AuthenticateRADIUS(string username, string password, string organization, string domain, string radiusPassword); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AuthenticateRADIUS", ReplyAction="*")] + System.Threading.Tasks.Task AuthenticateRADIUSAsync(string username, string password, string organization, string domain, string radiusPassword); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetTokenIsValid", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.TokenIsValidResult GetTokenIsValid(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetTokenIsValid", ReplyAction="*")] + System.Threading.Tasks.Task GetTokenIsValidAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretLegacy", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetSecretLegacy(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretLegacy", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretLegacyAsync(string token, int secretId); + + // CODEGEN: Parameter 'loadSettingsAndPermissions' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretResponse GetSecret(Alkami.Ops.SecretServer.SSWebService.GetSecretRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecret", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretAsync(Alkami.Ops.SecretServer.SSWebService.GetSecretRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetCheckOutStatus", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetCheckOutStatusResult GetCheckOutStatus(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetCheckOutStatus", ReplyAction="*")] + System.Threading.Tasks.Task GetCheckOutStatusAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ChangePassword", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult ChangePassword(string token, string currentPassword, string newPassword); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ChangePassword", ReplyAction="*")] + System.Threading.Tasks.Task ChangePasswordAsync(string token, string currentPassword, string newPassword); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretsByFieldValue", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretsByFieldValueResult GetSecretsByFieldValue(string token, string fieldName, string searchTerm, bool showDeleted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretsByFieldValue", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretsByFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFieldValue", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFieldValue", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsByFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretsByExposedFieldValue", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretsByFieldValueResult GetSecretsByExposedFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretsByExposedFieldValue", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretsByExposedFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByExposedFieldValue", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByExposedFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByExposedFieldValue", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsByExposedFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByExposedValues", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByExposedValues(string token, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByExposedValues", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsByExposedValuesAsync(string token, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddUser", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddUser(string token, Alkami.Ops.SecretServer.SSWebService.User newUser); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddUser", ReplyAction="*")] + System.Threading.Tasks.Task AddUserAsync(string token, Alkami.Ops.SecretServer.SSWebService.User newUser); + + // CODEGEN: Parameter 'includeDeleted' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecrets", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResponse SearchSecrets(Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecrets", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsLegacy", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsLegacy(string token, string searchTerm); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsLegacy", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsLegacyAsync(string token, string searchTerm); + + // CODEGEN: Parameter 'folderId' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFolder", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderResponse SearchSecretsByFolder(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFolder", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsByFolderAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest request); + + // CODEGEN: Parameter 'folderId' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFolderLegacy", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse SearchSecretsByFolderLegacy(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretsByFolderLegacy", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretsByFolderLegacyAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetFavorites", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetFavoritesResult GetFavorites(string token, bool includeRestricted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetFavorites", ReplyAction="*")] + System.Threading.Tasks.Task GetFavoritesAsync(string token, bool includeRestricted); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateIsFavorite", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateIsFavorite(string token, int secretId, bool isFavorite); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateIsFavorite", ReplyAction="*")] + System.Threading.Tasks.Task UpdateIsFavoriteAsync(string token, int secretId, bool isFavorite); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.AddSecretResult AddSecret(string token, int secretTypeId, string secretName, int[] secretFieldIds, string[] secretItemValues, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecret", ReplyAction="*")] + System.Threading.Tasks.Task AddSecretAsync(string token, int secretTypeId, string secretName, int[] secretFieldIds, string[] secretItemValues, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddNewSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.AddSecretResult AddNewSecret(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddNewSecret", ReplyAction="*")] + System.Threading.Tasks.Task AddNewSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetNewSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetNewSecret(string token, int secretTypeId, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetNewSecret", ReplyAction="*")] + System.Threading.Tasks.Task GetNewSecretAsync(string token, int secretTypeId, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretTemplateFields", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretTemplateFieldsResult GetSecretTemplateFields(string token, int secretTypeId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretTemplateFields", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretTemplateFieldsAsync(string token, int secretTypeId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateSecret(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateSecret", ReplyAction="*")] + System.Threading.Tasks.Task UpdateSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretTemplates", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretTemplatesResult GetSecretTemplates(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretTemplates", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretTemplatesAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GeneratePassword", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GeneratePasswordResult GeneratePassword(string token, int secretFieldId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GeneratePassword", ReplyAction="*")] + System.Threading.Tasks.Task GeneratePasswordAsync(string token, int secretFieldId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DeactivateSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult DeactivateSecret(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DeactivateSecret", ReplyAction="*")] + System.Threading.Tasks.Task DeactivateSecretAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/VersionGet", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.VersionGetResult VersionGet(); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/VersionGet", ReplyAction="*")] + System.Threading.Tasks.Task VersionGetAsync(); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderGet", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetFolderResult FolderGet(string token, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderGet", ReplyAction="*")] + System.Threading.Tasks.Task FolderGetAsync(string token, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderUpdate", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult FolderUpdate(string token, Alkami.Ops.SecretServer.SSWebService.Folder modifiedFolder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderUpdate", ReplyAction="*")] + System.Threading.Tasks.Task FolderUpdateAsync(string token, Alkami.Ops.SecretServer.SSWebService.Folder modifiedFolder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderGetAllChildren", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetFoldersResult FolderGetAllChildren(string token, int parentFolderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderGetAllChildren", ReplyAction="*")] + System.Threading.Tasks.Task FolderGetAllChildrenAsync(string token, int parentFolderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderCreate", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.CreateFolderResult FolderCreate(string token, string folderName, int parentFolderId, int folderTypeId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderCreate", ReplyAction="*")] + System.Threading.Tasks.Task FolderCreateAsync(string token, string folderName, int parentFolderId, int folderTypeId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedCreate", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FolderExtendedCreateResult FolderExtendedCreate(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedCreate", ReplyAction="*")] + System.Threading.Tasks.Task FolderExtendedCreateAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedGet", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetResult FolderExtendedGet(string token, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedGet", ReplyAction="*")] + System.Threading.Tasks.Task FolderExtendedGetAsync(string token, int folderId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedUpdate", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FolderExtendedUpdateResult FolderExtendedUpdate(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedUpdate", ReplyAction="*")] + System.Threading.Tasks.Task FolderExtendedUpdateAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedGetNew", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewResult FolderExtendedGetNew(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewRequest folderExtendedGetNewRequest); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/FolderExtendedGetNew", ReplyAction="*")] + System.Threading.Tasks.Task FolderExtendedGetNewAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewRequest folderExtendedGetNewRequest); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchFolders", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchFolderResult SearchFolders(string token, string folderName); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchFolders", ReplyAction="*")] + System.Threading.Tasks.Task SearchFoldersAsync(string token, string folderName); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DownloadFileAttachment", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FileDownloadResult DownloadFileAttachment(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DownloadFileAttachment", ReplyAction="*")] + System.Threading.Tasks.Task DownloadFileAttachmentAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DownloadFileAttachmentByItemId", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.FileDownloadResult DownloadFileAttachmentByItemId(string token, int secretId, int secretItemId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DownloadFileAttachmentByItemId", ReplyAction="*")] + System.Threading.Tasks.Task DownloadFileAttachmentByItemIdAsync(string token, int secretId, int secretItemId); + + // CODEGEN: Parameter 'fileData' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UploadFileAttachment", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentResponse UploadFileAttachment(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UploadFileAttachment", ReplyAction="*")] + System.Threading.Tasks.Task UploadFileAttachmentAsync(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest request); + + // CODEGEN: Parameter 'fileData' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UploadFileAttachmentByItemId", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse UploadFileAttachmentByItemId(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UploadFileAttachmentByItemId", ReplyAction="*")] + System.Threading.Tasks.Task UploadFileAttachmentByItemIdAsync(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ExpireSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult ExpireSecret(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ExpireSecret", ReplyAction="*")] + System.Threading.Tasks.Task ExpireSecretAsync(string token, int secretId); + + // CODEGEN: Parameter 'checkOutInterval' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SetCheckOutEnabled", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledResponse SetCheckOutEnabled(Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SetCheckOutEnabled", ReplyAction="*")] + System.Threading.Tasks.Task SetCheckOutEnabledAsync(Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ImportXML", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult ImportXML(string token, string xml); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/ImportXML", ReplyAction="*")] + System.Threading.Tasks.Task ImportXMLAsync(string token, string xml); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretAudit", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSecretAuditResult GetSecretAudit(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretAudit", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretAuditAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddDependency", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddDependency(string token, Alkami.Ops.SecretServer.SSWebService.Dependency dependency); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddDependency", ReplyAction="*")] + System.Threading.Tasks.Task AddDependencyAsync(string token, Alkami.Ops.SecretServer.SSWebService.Dependency dependency); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RemoveDependency", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult RemoveDependency(string token, int dependencyId, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RemoveDependency", ReplyAction="*")] + System.Threading.Tasks.Task RemoveDependencyAsync(string token, int dependencyId, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDependencies", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetDependenciesResult GetDependencies(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDependencies", ReplyAction="*")] + System.Threading.Tasks.Task GetDependenciesAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CreateDependencyGroupForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult CreateDependencyGroupForSecret(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CreateDependencyGroupForSecret", ReplyAction="*")] + System.Threading.Tasks.Task CreateDependencyGroupForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDependencyGroupsForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetDependencyGroupsResult GetDependencyGroupsForSecret(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDependencyGroupsForSecret", ReplyAction="*")] + System.Threading.Tasks.Task GetDependencyGroupsForSecretAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateDependencyGroupForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateDependencyGroupForSecret(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateDependencyGroupForSecret", ReplyAction="*")] + System.Threading.Tasks.Task UpdateDependencyGroupForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RemoveDependencyGroupForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult RemoveDependencyGroupForSecret(string token, int dependencyGroupId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RemoveDependencyGroupForSecret", ReplyAction="*")] + System.Threading.Tasks.Task RemoveDependencyGroupForSecretAsync(string token, int dependencyGroupId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDistributedEngines", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSitesResult GetDistributedEngines(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetDistributedEngines", ReplyAction="*")] + System.Threading.Tasks.Task GetDistributedEnginesAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetTicketSystems", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetTicketSystemsResult GetTicketSystems(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetTicketSystems", ReplyAction="*")] + System.Threading.Tasks.Task GetTicketSystemsAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignSite", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AssignSite(string token, int secretId, int siteId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignSite", ReplyAction="*")] + System.Threading.Tasks.Task AssignSiteAsync(string token, int secretId, int siteId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CheckIn", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult CheckIn(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CheckIn", ReplyAction="*")] + System.Threading.Tasks.Task CheckInAsync(string token, int secretId); + + // CODEGEN: Parameter 'referenceId' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecretCustomAudit", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditResponse AddSecretCustomAudit(Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecretCustomAudit", ReplyAction="*")] + System.Threading.Tasks.Task AddSecretCustomAuditAsync(Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateSecretPermission", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateSecretPermission(string token, int secretId, Alkami.Ops.SecretServer.SSWebService.GroupOrUserRecord groupOrUserRecord, bool view, bool edit, bool owner); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateSecretPermission", ReplyAction="*")] + System.Threading.Tasks.Task UpdateSecretPermissionAsync(string token, int secretId, Alkami.Ops.SecretServer.SSWebService.GroupOrUserRecord groupOrUserRecord, bool view, bool edit, bool owner); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CheckInByKey", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult CheckInByKey(string sessionKey); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/CheckInByKey", ReplyAction="*")] + System.Threading.Tasks.Task CheckInByKeyAsync(string sessionKey); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/WhoAmI", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.UserInfoResult WhoAmI(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/WhoAmI", ReplyAction="*")] + System.Threading.Tasks.Task WhoAmIAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllGroups", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetAllGroupsResult GetAllGroups(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllGroups", ReplyAction="*")] + System.Threading.Tasks.Task GetAllGroupsAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignUserToGroup", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AssignUserToGroup(string token, int userId, int groupId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignUserToGroup", ReplyAction="*")] + System.Threading.Tasks.Task AssignUserToGroupAsync(string token, int userId, int groupId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHLoginCredentials", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SSHCredentialsResult GetSSHLoginCredentials(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHLoginCredentials", ReplyAction="*")] + System.Threading.Tasks.Task GetSSHLoginCredentialsAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHLoginCredentialsWithMachine", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SSHCredentialsResult GetSSHLoginCredentialsWithMachine(string token, int secretId, string machine); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHLoginCredentialsWithMachine", ReplyAction="*")] + System.Threading.Tasks.Task GetSSHLoginCredentialsWithMachineAsync(string token, int secretId, string machine); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchUsers", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetUsersResult SearchUsers(string token, string searchTerm, bool includeInactiveUsers); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchUsers", ReplyAction="*")] + System.Threading.Tasks.Task SearchUsersAsync(string token, string searchTerm, bool includeInactiveUsers); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetUser", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetUserResult GetUser(string token, int userId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetUser", ReplyAction="*")] + System.Threading.Tasks.Task GetUserAsync(string token, int userId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateUser", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.UpdateUserResult UpdateUser(string token, Alkami.Ops.SecretServer.SSWebService.User user); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateUser", ReplyAction="*")] + System.Threading.Tasks.Task UpdateUserAsync(string token, Alkami.Ops.SecretServer.SSWebService.User user); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretItemHistoryByFieldName", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SecretItemHistoryResult GetSecretItemHistoryByFieldName(string token, int secretId, string fieldDisplayName); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretItemHistoryByFieldName", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretItemHistoryByFieldNameAsync(string token, int secretId, string fieldDisplayName); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretPolicyForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecretResult GetSecretPolicyForSecret(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSecretPolicyForSecret", ReplyAction="*")] + System.Threading.Tasks.Task GetSecretPolicyForSecretAsync(string token, int secretId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignSecretPolicyForSecret", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecretResult AssignSecretPolicyForSecret(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecret secretPolicyForSecret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AssignSecretPolicyForSecret", ReplyAction="*")] + System.Threading.Tasks.Task AssignSecretPolicyForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecret secretPolicyForSecret); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretPolicies", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SearchSecretPoliciesResult SearchSecretPolicies(string token, string term, bool includeInactive); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SearchSecretPolicies", ReplyAction="*")] + System.Threading.Tasks.Task SearchSecretPoliciesAsync(string token, string term, bool includeInactive); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RunActiveDirectorySynchronization", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult RunActiveDirectorySynchronization(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RunActiveDirectorySynchronization", ReplyAction="*")] + System.Threading.Tasks.Task RunActiveDirectorySynchronizationAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddGroupToActiveDirectorySynchronization", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddGroupToActiveDirectorySynchronization(string token, Alkami.Ops.SecretServer.SSWebService.AddGroupRequestMessage addGroupRequestMessage); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddGroupToActiveDirectorySynchronization", ReplyAction="*")] + System.Threading.Tasks.Task AddGroupToActiveDirectorySynchronizationAsync(string token, Alkami.Ops.SecretServer.SSWebService.AddGroupRequestMessage addGroupRequestMessage); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecretPolicy", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SecretPolicyResult AddSecretPolicy(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyDetail secretPolicy); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddSecretPolicy", ReplyAction="*")] + System.Threading.Tasks.Task AddSecretPolicyAsync(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyDetail secretPolicy); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetNewSecretPolicy", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.SecretPolicyResult GetNewSecretPolicy(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetNewSecretPolicy", ReplyAction="*")] + System.Threading.Tasks.Task GetNewSecretPolicyAsync(string token); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHCommandMenu", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenuResult GetSSHCommandMenu(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetSSHCommandMenu", ReplyAction="*")] + System.Threading.Tasks.Task GetSSHCommandMenuAsync(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SaveSSHCommandMenu", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenuResult SaveSSHCommandMenu(string token, Alkami.Ops.SecretServer.SSWebService.SshCommandMenu sshCommandMenu, string commandsText, bool deleteCommands); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/SaveSSHCommandMenu", ReplyAction="*")] + System.Threading.Tasks.Task SaveSSHCommandMenuAsync(string token, Alkami.Ops.SecretServer.SSWebService.SshCommandMenu sshCommandMenu, string commandsText, bool deleteCommands); + + // CODEGEN: Parameter 'includeInactive' requires additional schema information that cannot be captured using the parameter mode. The specific attribute is 'System.Xml.Serialization.XmlElementAttribute'. + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllSSHCommandMenus", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusResponse GetAllSSHCommandMenus(Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllSSHCommandMenus", ReplyAction="*")] + System.Threading.Tasks.Task GetAllSSHCommandMenusAsync(Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest request); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DeleteSSHCommandMenu", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult DeleteSSHCommandMenu(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/DeleteSSHCommandMenu", ReplyAction="*")] + System.Threading.Tasks.Task DeleteSSHCommandMenuAsync(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RestoreSSHCommandMenu", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult RestoreSSHCommandMenu(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/RestoreSSHCommandMenu", ReplyAction="*")] + System.Threading.Tasks.Task RestoreSSHCommandMenuAsync(string token, int sshCommandMenuId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddScript", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddScript(string token, Alkami.Ops.SecretServer.SSWebService.UserScript newUserScript); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/AddScript", ReplyAction="*")] + System.Threading.Tasks.Task AddScriptAsync(string token, Alkami.Ops.SecretServer.SSWebService.UserScript newUserScript); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllScripts", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetUserScriptsResult GetAllScripts(string token, bool includeInactiveUserScripts); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetAllScripts", ReplyAction="*")] + System.Threading.Tasks.Task GetAllScriptsAsync(string token, bool includeInactiveUserScripts); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetScript", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.GetUserScriptResult GetScript(string token, int userScriptId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/GetScript", ReplyAction="*")] + System.Threading.Tasks.Task GetScriptAsync(string token, int userScriptId); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateScript", ReplyAction="*")] + [System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(FolderExtendedResultBase))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(GenericResult))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(SqlScriptArgument2[]))] + [System.ServiceModel.ServiceKnownTypeAttribute(typeof(UserScript[]))] + Alkami.Ops.SecretServer.SSWebService.UpdateUserScriptResult UpdateScript(string token, Alkami.Ops.SecretServer.SSWebService.UserScript userScript); + + [System.ServiceModel.OperationContractAttribute(Action="urn:thesecretserver.com/UpdateScript", ReplyAction="*")] + System.Threading.Tasks.Task UpdateScriptAsync(string token, Alkami.Ops.SecretServer.SSWebService.UserScript userScript); + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class RequestApprovalResult : GenericResult { + + private ApprovalInfo approvalInfoField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public ApprovalInfo ApprovalInfo { + get { + return this.approvalInfoField; + } + set { + this.approvalInfoField = value; + this.RaisePropertyChanged("ApprovalInfo"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class ApprovalInfo : object, System.ComponentModel.INotifyPropertyChanged { + + private SecretAccessRequestStatus statusField; + + private string responderField; + + private System.DateTime responseDateField; + + private string responseCommentField; + + private System.Nullable expirationDateField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public SecretAccessRequestStatus Status { + get { + return this.statusField; + } + set { + this.statusField = value; + this.RaisePropertyChanged("Status"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Responder { + get { + return this.responderField; + } + set { + this.responderField = value; + this.RaisePropertyChanged("Responder"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public System.DateTime ResponseDate { + get { + return this.responseDateField; + } + set { + this.responseDateField = value; + this.RaisePropertyChanged("ResponseDate"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string ResponseComment { + get { + return this.responseCommentField; + } + set { + this.responseCommentField = value; + this.RaisePropertyChanged("ResponseComment"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=4)] + public System.Nullable ExpirationDate { + get { + return this.expirationDateField; + } + set { + this.expirationDateField = value; + this.RaisePropertyChanged("ExpirationDate"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum SecretAccessRequestStatus { + + /// + Pending, + + /// + Approved, + + /// + Denied, + + /// + Canceled, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshScriptArgument2 : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private string valueField; + + private SshArgumentType2 sshTypeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public SshArgumentType2 SshType { + get { + return this.sshTypeField; + } + set { + this.sshTypeField = value; + this.RaisePropertyChanged("SshType"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum SshArgumentType2 { + + /// + Interpreted, + + /// + Literal, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AdditionalDataSshObject : object, System.ComponentModel.INotifyPropertyChanged { + + private string portField; + + private LineEnding lineEndingField; + + private bool doNotUseEnvironmentField; + + private SshScriptArgument2[] paramsField; + + private int versionField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public LineEnding LineEnding { + get { + return this.lineEndingField; + } + set { + this.lineEndingField = value; + this.RaisePropertyChanged("LineEnding"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool DoNotUseEnvironment { + get { + return this.doNotUseEnvironmentField; + } + set { + this.doNotUseEnvironmentField = value; + this.RaisePropertyChanged("DoNotUseEnvironment"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=3)] + public SshScriptArgument2[] Params { + get { + return this.paramsField; + } + set { + this.paramsField = value; + this.RaisePropertyChanged("Params"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int Version { + get { + return this.versionField; + } + set { + this.versionField = value; + this.RaisePropertyChanged("Version"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum LineEnding { + + /// + NewLine, + + /// + CarriageReturn, + + /// + CarriageReturnNewLine, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSshCommandMenusResult : object, System.ComponentModel.INotifyPropertyChanged { + + private SshCommandMenu[] sshCommandMenusField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public SshCommandMenu[] SshCommandMenus { + get { + return this.sshCommandMenusField; + } + set { + this.sshCommandMenusField = value; + this.RaisePropertyChanged("SshCommandMenus"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshCommandMenu : object, System.ComponentModel.INotifyPropertyChanged { + + private int sshCommandMenuIdField; + + private string nameField; + + private bool activeField; + + private string descriptionField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SshCommandMenuId { + get { + return this.sshCommandMenuIdField; + } + set { + this.sshCommandMenuIdField = value; + this.RaisePropertyChanged("SshCommandMenuId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string Description { + get { + return this.descriptionField; + } + set { + this.descriptionField = value; + this.RaisePropertyChanged("Description"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSshCommandMenuResult : object, System.ComponentModel.INotifyPropertyChanged { + + private SshCommandMenu sshCommandMenuField; + + private string sshCommandsField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public SshCommandMenu SshCommandMenu { + get { + return this.sshCommandMenuField; + } + set { + this.sshCommandMenuField = value; + this.RaisePropertyChanged("SshCommandMenu"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string SshCommands { + get { + return this.sshCommandsField; + } + set { + this.sshCommandsField = value; + this.RaisePropertyChanged("SshCommands"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=2)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AddGroupRequestMessage : object, System.ComponentModel.INotifyPropertyChanged { + + private string groupNameField; + + private System.Nullable domainIdField; + + private string domainNameField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string GroupName { + get { + return this.groupNameField; + } + set { + this.groupNameField = value; + this.RaisePropertyChanged("GroupName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable DomainId { + get { + return this.domainIdField; + } + set { + this.domainIdField = value; + this.RaisePropertyChanged("DomainId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string DomainName { + get { + return this.domainNameField; + } + set { + this.domainNameField = value; + this.RaisePropertyChanged("DomainName"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretItemHistoryWebServiceResult : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretItemHistoryIdField; + + private int userIdField; + + private int secretItemIdField; + + private int secretIdField; + + private System.DateTime dateField; + + private string itemValueNewField; + + private string itemValueNew2Field; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretItemHistoryId { + get { + return this.secretItemHistoryIdField; + } + set { + this.secretItemHistoryIdField = value; + this.RaisePropertyChanged("SecretItemHistoryId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int UserId { + get { + return this.userIdField; + } + set { + this.userIdField = value; + this.RaisePropertyChanged("UserId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int SecretItemId { + get { + return this.secretItemIdField; + } + set { + this.secretItemIdField = value; + this.RaisePropertyChanged("SecretItemId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public System.DateTime Date { + get { + return this.dateField; + } + set { + this.dateField = value; + this.RaisePropertyChanged("Date"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public string ItemValueNew { + get { + return this.itemValueNewField; + } + set { + this.itemValueNewField = value; + this.RaisePropertyChanged("ItemValueNew"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public string ItemValueNew2 { + get { + return this.itemValueNew2Field; + } + set { + this.itemValueNew2Field = value; + this.RaisePropertyChanged("ItemValueNew2"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretItemHistoryResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private bool successField; + + private SecretItemHistoryWebServiceResult[] secretItemHistoriesField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=2)] + public SecretItemHistoryWebServiceResult[] SecretItemHistories { + get { + return this.secretItemHistoriesField; + } + set { + this.secretItemHistoriesField = value; + this.RaisePropertyChanged("SecretItemHistories"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class UpdateUserResult : object, System.ComponentModel.INotifyPropertyChanged { + + private User userField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public User User { + get { + return this.userField; + } + set { + this.userField = value; + this.RaisePropertyChanged("User"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class User : object, System.ComponentModel.INotifyPropertyChanged { + + private System.Nullable idField; + + private string userNameField; + + private string displayNameField; + + private System.Nullable domainIdField; + + private bool isApplicationAccountField; + + private bool radiusTwoFactorField; + + private bool emailTwoFactorField; + + private string radiusUserNameField; + + private string emailAddressField; + + private string passwordField; + + private bool enabledField; + + private bool duoTwoFactorField; + + private bool oATHTwoFactorField; + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=0)] + public System.Nullable Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string UserName { + get { + return this.userNameField; + } + set { + this.userNameField = value; + this.RaisePropertyChanged("UserName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string DisplayName { + get { + return this.displayNameField; + } + set { + this.displayNameField = value; + this.RaisePropertyChanged("DisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable DomainId { + get { + return this.domainIdField; + } + set { + this.domainIdField = value; + this.RaisePropertyChanged("DomainId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public bool IsApplicationAccount { + get { + return this.isApplicationAccountField; + } + set { + this.isApplicationAccountField = value; + this.RaisePropertyChanged("IsApplicationAccount"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool RadiusTwoFactor { + get { + return this.radiusTwoFactorField; + } + set { + this.radiusTwoFactorField = value; + this.RaisePropertyChanged("RadiusTwoFactor"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public bool EmailTwoFactor { + get { + return this.emailTwoFactorField; + } + set { + this.emailTwoFactorField = value; + this.RaisePropertyChanged("EmailTwoFactor"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public string RadiusUserName { + get { + return this.radiusUserNameField; + } + set { + this.radiusUserNameField = value; + this.RaisePropertyChanged("RadiusUserName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public string EmailAddress { + get { + return this.emailAddressField; + } + set { + this.emailAddressField = value; + this.RaisePropertyChanged("EmailAddress"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public string Password { + get { + return this.passwordField; + } + set { + this.passwordField = value; + this.RaisePropertyChanged("Password"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=10)] + public bool Enabled { + get { + return this.enabledField; + } + set { + this.enabledField = value; + this.RaisePropertyChanged("Enabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=11)] + public bool DuoTwoFactor { + get { + return this.duoTwoFactorField; + } + set { + this.duoTwoFactorField = value; + this.RaisePropertyChanged("DuoTwoFactor"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=12)] + public bool OATHTwoFactor { + get { + return this.oATHTwoFactorField; + } + set { + this.oATHTwoFactorField = value; + this.RaisePropertyChanged("OATHTwoFactor"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetUserResult : object, System.ComponentModel.INotifyPropertyChanged { + + private User userField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public User User { + get { + return this.userField; + } + set { + this.userField = value; + this.RaisePropertyChanged("User"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetUsersResult : object, System.ComponentModel.INotifyPropertyChanged { + + private User[] usersField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public User[] Users { + get { + return this.usersField; + } + set { + this.usersField = value; + this.RaisePropertyChanged("Users"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Group : object, System.ComponentModel.INotifyPropertyChanged { + + private int idField; + + private string nameField; + + private int domainIdField; + + private string domainNameField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int DomainId { + get { + return this.domainIdField; + } + set { + this.domainIdField = value; + this.RaisePropertyChanged("DomainId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string DomainName { + get { + return this.domainNameField; + } + set { + this.domainNameField = value; + this.RaisePropertyChanged("DomainName"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetAllGroupsResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private Group[] groupsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public Group[] Groups { + get { + return this.groupsField; + } + set { + this.groupsField = value; + this.RaisePropertyChanged("Groups"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class UserInfoResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private string displayNameField; + + private string userNameField; + + private string knownAsField; + + private int userIdField; + + private int domainIdField; + + private string domainNameField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string DisplayName { + get { + return this.displayNameField; + } + set { + this.displayNameField = value; + this.RaisePropertyChanged("DisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string UserName { + get { + return this.userNameField; + } + set { + this.userNameField = value; + this.RaisePropertyChanged("UserName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string KnownAs { + get { + return this.knownAsField; + } + set { + this.knownAsField = value; + this.RaisePropertyChanged("KnownAs"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int UserId { + get { + return this.userIdField; + } + set { + this.userIdField = value; + this.RaisePropertyChanged("UserId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public int DomainId { + get { + return this.domainIdField; + } + set { + this.domainIdField = value; + this.RaisePropertyChanged("DomainId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public string DomainName { + get { + return this.domainNameField; + } + set { + this.domainNameField = value; + this.RaisePropertyChanged("DomainName"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class TicketSystem : object, System.ComponentModel.INotifyPropertyChanged { + + private int ticketSystemIdField; + + private string nameField; + + private string descriptionField; + + private bool isDefaultField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int TicketSystemId { + get { + return this.ticketSystemIdField; + } + set { + this.ticketSystemIdField = value; + this.RaisePropertyChanged("TicketSystemId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Description { + get { + return this.descriptionField; + } + set { + this.descriptionField = value; + this.RaisePropertyChanged("Description"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public bool IsDefault { + get { + return this.isDefaultField; + } + set { + this.isDefaultField = value; + this.RaisePropertyChanged("IsDefault"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetTicketSystemsResult : object, System.ComponentModel.INotifyPropertyChanged { + + private TicketSystem[] ticketSystemsField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public TicketSystem[] TicketSystems { + get { + return this.ticketSystemsField; + } + set { + this.ticketSystemsField = value; + this.RaisePropertyChanged("TicketSystems"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SiteConnector : object, System.ComponentModel.INotifyPropertyChanged { + + private int siteConnectorIdField; + + private string siteConnectorNameField; + + private string queueTypeField; + + private string hostNameField; + + private int portField; + + private bool activeField; + + private bool validatedField; + + private bool useSslField; + + private string sslCertificateThumbprintField; + + private System.DateTime lastModifiedDateField; + + private string userNameField; + + private byte[] passwordIVField; + + private string versionField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SiteConnectorId { + get { + return this.siteConnectorIdField; + } + set { + this.siteConnectorIdField = value; + this.RaisePropertyChanged("SiteConnectorId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string SiteConnectorName { + get { + return this.siteConnectorNameField; + } + set { + this.siteConnectorNameField = value; + this.RaisePropertyChanged("SiteConnectorName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string QueueType { + get { + return this.queueTypeField; + } + set { + this.queueTypeField = value; + this.RaisePropertyChanged("QueueType"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string HostName { + get { + return this.hostNameField; + } + set { + this.hostNameField = value; + this.RaisePropertyChanged("HostName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public bool Validated { + get { + return this.validatedField; + } + set { + this.validatedField = value; + this.RaisePropertyChanged("Validated"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public bool UseSsl { + get { + return this.useSslField; + } + set { + this.useSslField = value; + this.RaisePropertyChanged("UseSsl"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public string SslCertificateThumbprint { + get { + return this.sslCertificateThumbprintField; + } + set { + this.sslCertificateThumbprintField = value; + this.RaisePropertyChanged("SslCertificateThumbprint"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public System.DateTime LastModifiedDate { + get { + return this.lastModifiedDateField; + } + set { + this.lastModifiedDateField = value; + this.RaisePropertyChanged("LastModifiedDate"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=10)] + public string UserName { + get { + return this.userNameField; + } + set { + this.userNameField = value; + this.RaisePropertyChanged("UserName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary", Order=11)] + public byte[] PasswordIV { + get { + return this.passwordIVField; + } + set { + this.passwordIVField = value; + this.RaisePropertyChanged("PasswordIV"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=12)] + public string Version { + get { + return this.versionField; + } + set { + this.versionField = value; + this.RaisePropertyChanged("Version"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Site : object, System.ComponentModel.INotifyPropertyChanged { + + private int siteIdField; + + private int organizationIdField; + + private string symmetricKeyField; + + private byte[] symmetricKeyIVField; + + private byte[] initializationVectorField; + + private string siteNameField; + + private bool activeField; + + private int heartbeatIntervalField; + + private bool useWebSiteField; + + private bool systemSiteField; + + private bool enableProxyField; + + private System.Nullable portField; + + private System.DateTime lastModifiedDateField; + + private string winRMEndpointField; + + private System.Nullable enableCredSSPForWinRMField; + + private int siteConnectorIdField; + + private SiteConnector siteConnectorField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SiteId { + get { + return this.siteIdField; + } + set { + this.siteIdField = value; + this.RaisePropertyChanged("SiteId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int OrganizationId { + get { + return this.organizationIdField; + } + set { + this.organizationIdField = value; + this.RaisePropertyChanged("OrganizationId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string SymmetricKey { + get { + return this.symmetricKeyField; + } + set { + this.symmetricKeyField = value; + this.RaisePropertyChanged("SymmetricKey"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary", Order=3)] + public byte[] SymmetricKeyIV { + get { + return this.symmetricKeyIVField; + } + set { + this.symmetricKeyIVField = value; + this.RaisePropertyChanged("SymmetricKeyIV"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary", Order=4)] + public byte[] InitializationVector { + get { + return this.initializationVectorField; + } + set { + this.initializationVectorField = value; + this.RaisePropertyChanged("InitializationVector"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public string SiteName { + get { + return this.siteNameField; + } + set { + this.siteNameField = value; + this.RaisePropertyChanged("SiteName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public int HeartbeatInterval { + get { + return this.heartbeatIntervalField; + } + set { + this.heartbeatIntervalField = value; + this.RaisePropertyChanged("HeartbeatInterval"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public bool UseWebSite { + get { + return this.useWebSiteField; + } + set { + this.useWebSiteField = value; + this.RaisePropertyChanged("UseWebSite"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public bool SystemSite { + get { + return this.systemSiteField; + } + set { + this.systemSiteField = value; + this.RaisePropertyChanged("SystemSite"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=10)] + public bool EnableProxy { + get { + return this.enableProxyField; + } + set { + this.enableProxyField = value; + this.RaisePropertyChanged("EnableProxy"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=11)] + public System.Nullable Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=12)] + public System.DateTime LastModifiedDate { + get { + return this.lastModifiedDateField; + } + set { + this.lastModifiedDateField = value; + this.RaisePropertyChanged("LastModifiedDate"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=13)] + public string WinRMEndpoint { + get { + return this.winRMEndpointField; + } + set { + this.winRMEndpointField = value; + this.RaisePropertyChanged("WinRMEndpoint"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=14)] + public System.Nullable EnableCredSSPForWinRM { + get { + return this.enableCredSSPForWinRMField; + } + set { + this.enableCredSSPForWinRMField = value; + this.RaisePropertyChanged("EnableCredSSPForWinRM"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=15)] + public int SiteConnectorId { + get { + return this.siteConnectorIdField; + } + set { + this.siteConnectorIdField = value; + this.RaisePropertyChanged("SiteConnectorId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=16)] + public SiteConnector SiteConnector { + get { + return this.siteConnectorField; + } + set { + this.siteConnectorField = value; + this.RaisePropertyChanged("SiteConnector"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSitesResult : object, System.ComponentModel.INotifyPropertyChanged { + + private Site[] sitesField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public Site[] Sites { + get { + return this.sitesField; + } + set { + this.sitesField = value; + this.RaisePropertyChanged("Sites"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetDependencyGroupsResult : object, System.ComponentModel.INotifyPropertyChanged { + + private DependencyGroup[] dependencyGroupsField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public DependencyGroup[] DependencyGroups { + get { + return this.dependencyGroupsField; + } + set { + this.dependencyGroupsField = value; + this.RaisePropertyChanged("DependencyGroups"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class DependencyGroup : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretDependencyGroupIdField; + + private int secretIdField; + + private string nameField; + + private System.Nullable siteIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretDependencyGroupId { + get { + return this.secretDependencyGroupIdField; + } + set { + this.secretDependencyGroupIdField = value; + this.RaisePropertyChanged("SecretDependencyGroupId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable SiteId { + get { + return this.siteIdField; + } + set { + this.siteIdField = value; + this.RaisePropertyChanged("SiteId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetDependenciesResult : object, System.ComponentModel.INotifyPropertyChanged { + + private Dependency[] dependenciesField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public Dependency[] Dependencies { + get { + return this.dependenciesField; + } + set { + this.dependenciesField = value; + this.RaisePropertyChanged("Dependencies"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Dependency : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretIdField; + + private int secretDependencyTypeIdField; + + private string machineNameField; + + private string serviceNameField; + + private int privilegedAccountSecretIdField; + + private bool activeField; + + private bool restartOnPasswordChangeField; + + private int waitBeforeSecondsField; + + private AdditionalDependencyInfoJson additionalInfoField; + + private string descriptionField; + + private int scriptIdField; + + private int secretDependencyIdField; + + private System.Nullable sSHKeySecretIdField; + + private System.Nullable secretDependencyTemplateIdField; + + private System.Nullable secretDependencyGroupIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int SecretDependencyTypeId { + get { + return this.secretDependencyTypeIdField; + } + set { + this.secretDependencyTypeIdField = value; + this.RaisePropertyChanged("SecretDependencyTypeId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string MachineName { + get { + return this.machineNameField; + } + set { + this.machineNameField = value; + this.RaisePropertyChanged("MachineName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string ServiceName { + get { + return this.serviceNameField; + } + set { + this.serviceNameField = value; + this.RaisePropertyChanged("ServiceName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int PrivilegedAccountSecretId { + get { + return this.privilegedAccountSecretIdField; + } + set { + this.privilegedAccountSecretIdField = value; + this.RaisePropertyChanged("PrivilegedAccountSecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public bool RestartOnPasswordChange { + get { + return this.restartOnPasswordChangeField; + } + set { + this.restartOnPasswordChangeField = value; + this.RaisePropertyChanged("RestartOnPasswordChange"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public int WaitBeforeSeconds { + get { + return this.waitBeforeSecondsField; + } + set { + this.waitBeforeSecondsField = value; + this.RaisePropertyChanged("WaitBeforeSeconds"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public AdditionalDependencyInfoJson AdditionalInfo { + get { + return this.additionalInfoField; + } + set { + this.additionalInfoField = value; + this.RaisePropertyChanged("AdditionalInfo"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public string Description { + get { + return this.descriptionField; + } + set { + this.descriptionField = value; + this.RaisePropertyChanged("Description"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=10)] + public int ScriptId { + get { + return this.scriptIdField; + } + set { + this.scriptIdField = value; + this.RaisePropertyChanged("ScriptId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=11)] + public int SecretDependencyId { + get { + return this.secretDependencyIdField; + } + set { + this.secretDependencyIdField = value; + this.RaisePropertyChanged("SecretDependencyId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=12)] + public System.Nullable SSHKeySecretId { + get { + return this.sSHKeySecretIdField; + } + set { + this.sSHKeySecretIdField = value; + this.RaisePropertyChanged("SSHKeySecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=13)] + public System.Nullable SecretDependencyTemplateId { + get { + return this.secretDependencyTemplateIdField; + } + set { + this.secretDependencyTemplateIdField = value; + this.RaisePropertyChanged("SecretDependencyTemplateId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=14)] + public System.Nullable SecretDependencyGroupId { + get { + return this.secretDependencyGroupIdField; + } + set { + this.secretDependencyGroupIdField = value; + this.RaisePropertyChanged("SecretDependencyGroupId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AdditionalDependencyInfoJson : object, System.ComponentModel.INotifyPropertyChanged { + + private string regexField; + + private string powershellArgumentsField; + + private SshScriptArgument[] sshArgumentsField; + + private SqlScriptArgument[] sqlArgumentsField; + + private OdbcConnectionArg[] odbcConnectionArgumentsField; + + private DependencyScanItemField[] dependencyScanItemFieldsField; + + private string portField; + + private string databaseField; + + private string serverKeyDigestField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Regex { + get { + return this.regexField; + } + set { + this.regexField = value; + this.RaisePropertyChanged("Regex"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string PowershellArguments { + get { + return this.powershellArgumentsField; + } + set { + this.powershellArgumentsField = value; + this.RaisePropertyChanged("PowershellArguments"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=2)] + public SshScriptArgument[] SshArguments { + get { + return this.sshArgumentsField; + } + set { + this.sshArgumentsField = value; + this.RaisePropertyChanged("SshArguments"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=3)] + public SqlScriptArgument[] SqlArguments { + get { + return this.sqlArgumentsField; + } + set { + this.sqlArgumentsField = value; + this.RaisePropertyChanged("SqlArguments"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=4)] + public OdbcConnectionArg[] OdbcConnectionArguments { + get { + return this.odbcConnectionArgumentsField; + } + set { + this.odbcConnectionArgumentsField = value; + this.RaisePropertyChanged("OdbcConnectionArguments"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=5)] + public DependencyScanItemField[] DependencyScanItemFields { + get { + return this.dependencyScanItemFieldsField; + } + set { + this.dependencyScanItemFieldsField = value; + this.RaisePropertyChanged("DependencyScanItemFields"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public string Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public string Database { + get { + return this.databaseField; + } + set { + this.databaseField = value; + this.RaisePropertyChanged("Database"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public string ServerKeyDigest { + get { + return this.serverKeyDigestField; + } + set { + this.serverKeyDigestField = value; + this.RaisePropertyChanged("ServerKeyDigest"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshScriptArgument : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private string valueField; + + private SshArgumentType sshTypeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public SshArgumentType SshType { + get { + return this.sshTypeField; + } + set { + this.sshTypeField = value; + this.RaisePropertyChanged("SshType"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum SshArgumentType { + + /// + Interpreted, + + /// + Literal, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SqlScriptArgument : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private object valueField; + + private DbType dbTypeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public object Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public DbType DbType { + get { + return this.dbTypeField; + } + set { + this.dbTypeField = value; + this.RaisePropertyChanged("DbType"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum DbType { + + /// + AnsiString, + + /// + Binary, + + /// + Byte, + + /// + Boolean, + + /// + Currency, + + /// + Date, + + /// + DateTime, + + /// + Decimal, + + /// + Double, + + /// + Guid, + + /// + Int16, + + /// + Int32, + + /// + Int64, + + /// + Object, + + /// + SByte, + + /// + Single, + + /// + String, + + /// + Time, + + /// + UInt16, + + /// + UInt32, + + /// + UInt64, + + /// + VarNumeric, + + /// + AnsiStringFixedLength, + + /// + StringFixedLength, + + /// + Xml, + + /// + DateTime2, + + /// + DateTimeOffset, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class OdbcConnectionArg : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private string valueField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class DependencyScanItemField : object, System.ComponentModel.INotifyPropertyChanged { + + private int scanItemFieldIdField; + + private string nameField; + + private string valueField; + + private string parentNameField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int ScanItemFieldId { + get { + return this.scanItemFieldIdField; + } + set { + this.scanItemFieldIdField = value; + this.RaisePropertyChanged("ScanItemFieldId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string ParentName { + get { + return this.parentNameField; + } + set { + this.parentNameField = value; + this.RaisePropertyChanged("ParentName"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AuditSecret : object, System.ComponentModel.INotifyPropertyChanged { + + private int auditSecretIdField; + + private int secretIdField; + + private System.DateTime dateRecordedField; + + private string actionField; + + private string notesField; + + private int userIdField; + + private string secretNameField; + + private string ipAddressField; + + private int referenceIdField; + + private string byUserDisplayNameField; + + private string ticketNumberField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int AuditSecretId { + get { + return this.auditSecretIdField; + } + set { + this.auditSecretIdField = value; + this.RaisePropertyChanged("AuditSecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public System.DateTime DateRecorded { + get { + return this.dateRecordedField; + } + set { + this.dateRecordedField = value; + this.RaisePropertyChanged("DateRecorded"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string Action { + get { + return this.actionField; + } + set { + this.actionField = value; + this.RaisePropertyChanged("Action"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string Notes { + get { + return this.notesField; + } + set { + this.notesField = value; + this.RaisePropertyChanged("Notes"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public int UserId { + get { + return this.userIdField; + } + set { + this.userIdField = value; + this.RaisePropertyChanged("UserId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public string SecretName { + get { + return this.secretNameField; + } + set { + this.secretNameField = value; + this.RaisePropertyChanged("SecretName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public string IpAddress { + get { + return this.ipAddressField; + } + set { + this.ipAddressField = value; + this.RaisePropertyChanged("IpAddress"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public int ReferenceId { + get { + return this.referenceIdField; + } + set { + this.referenceIdField = value; + this.RaisePropertyChanged("ReferenceId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public string ByUserDisplayName { + get { + return this.byUserDisplayNameField; + } + set { + this.byUserDisplayNameField = value; + this.RaisePropertyChanged("ByUserDisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=10)] + public string TicketNumber { + get { + return this.ticketNumberField; + } + set { + this.ticketNumberField = value; + this.RaisePropertyChanged("TicketNumber"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSecretAuditResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private AuditSecret[] secretAuditsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public AuditSecret[] SecretAudits { + get { + return this.secretAuditsField; + } + set { + this.secretAuditsField = value; + this.RaisePropertyChanged("SecretAudits"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SearchFolderResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private Folder[] foldersField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public Folder[] Folders { + get { + return this.foldersField; + } + set { + this.foldersField = value; + this.RaisePropertyChanged("Folders"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FolderExtended))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Folder : object, System.ComponentModel.INotifyPropertyChanged { + + private int idField; + + private string nameField; + + private int typeIdField; + + private int parentFolderIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int TypeId { + get { + return this.typeIdField; + } + set { + this.typeIdField = value; + this.RaisePropertyChanged("TypeId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public int ParentFolderId { + get { + return this.parentFolderIdField; + } + set { + this.parentFolderIdField = value; + this.RaisePropertyChanged("ParentFolderId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtended : Folder { + + private FolderPermissions permissionSettingsField; + + private FolderSettings settingsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public FolderPermissions PermissionSettings { + get { + return this.permissionSettingsField; + } + set { + this.permissionSettingsField = value; + this.RaisePropertyChanged("PermissionSettings"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public FolderSettings Settings { + get { + return this.settingsField; + } + set { + this.settingsField = value; + this.RaisePropertyChanged("Settings"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderPermissions : object, System.ComponentModel.INotifyPropertyChanged { + + private System.Nullable isChangeToPermissionsField; + + private System.Nullable inheritPermissionsEnabledField; + + private FolderPermission[] permissionsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=0)] + public System.Nullable IsChangeToPermissions { + get { + return this.isChangeToPermissionsField; + } + set { + this.isChangeToPermissionsField = value; + this.RaisePropertyChanged("IsChangeToPermissions"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable InheritPermissionsEnabled { + get { + return this.inheritPermissionsEnabledField; + } + set { + this.inheritPermissionsEnabledField = value; + this.RaisePropertyChanged("InheritPermissionsEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=2)] + public FolderPermission[] Permissions { + get { + return this.permissionsField; + } + set { + this.permissionsField = value; + this.RaisePropertyChanged("Permissions"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderPermission : object, System.ComponentModel.INotifyPropertyChanged { + + private GroupOrUserRecord userOrGroupField; + + private string folderAccessRoleNameField; + + private System.Nullable folderAccessRoleIdField; + + private string secretAccessRoleNameField; + + private System.Nullable secretAccessRoleIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public GroupOrUserRecord UserOrGroup { + get { + return this.userOrGroupField; + } + set { + this.userOrGroupField = value; + this.RaisePropertyChanged("UserOrGroup"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string FolderAccessRoleName { + get { + return this.folderAccessRoleNameField; + } + set { + this.folderAccessRoleNameField = value; + this.RaisePropertyChanged("FolderAccessRoleName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=2)] + public System.Nullable FolderAccessRoleId { + get { + return this.folderAccessRoleIdField; + } + set { + this.folderAccessRoleIdField = value; + this.RaisePropertyChanged("FolderAccessRoleId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string SecretAccessRoleName { + get { + return this.secretAccessRoleNameField; + } + set { + this.secretAccessRoleNameField = value; + this.RaisePropertyChanged("SecretAccessRoleName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=4)] + public System.Nullable SecretAccessRoleId { + get { + return this.secretAccessRoleIdField; + } + set { + this.secretAccessRoleIdField = value; + this.RaisePropertyChanged("SecretAccessRoleId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GroupOrUserRecord : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private string domainNameField; + + private bool isUserField; + + private System.Nullable groupIdField; + + private System.Nullable userIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string DomainName { + get { + return this.domainNameField; + } + set { + this.domainNameField = value; + this.RaisePropertyChanged("DomainName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool IsUser { + get { + return this.isUserField; + } + set { + this.isUserField = value; + this.RaisePropertyChanged("IsUser"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable GroupId { + get { + return this.groupIdField; + } + set { + this.groupIdField = value; + this.RaisePropertyChanged("GroupId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=4)] + public System.Nullable UserId { + get { + return this.userIdField; + } + set { + this.userIdField = value; + this.RaisePropertyChanged("UserId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderSettings : object, System.ComponentModel.INotifyPropertyChanged { + + private System.Nullable isChangeToSettingsField; + + private System.Nullable inheritSecretPolicyField; + + private System.Nullable secretPolicyIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=0)] + public System.Nullable IsChangeToSettings { + get { + return this.isChangeToSettingsField; + } + set { + this.isChangeToSettingsField = value; + this.RaisePropertyChanged("IsChangeToSettings"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable InheritSecretPolicy { + get { + return this.inheritSecretPolicyField; + } + set { + this.inheritSecretPolicyField = value; + this.RaisePropertyChanged("InheritSecretPolicy"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=2)] + public System.Nullable SecretPolicyId { + get { + return this.secretPolicyIdField; + } + set { + this.secretPolicyIdField = value; + this.RaisePropertyChanged("SecretPolicyId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedGetNewRequest : object, System.ComponentModel.INotifyPropertyChanged { + + private string folderNameField; + + private System.Nullable parentFolderIdField; + + private System.Nullable inheritPermissionsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string FolderName { + get { + return this.folderNameField; + } + set { + this.folderNameField = value; + this.RaisePropertyChanged("FolderName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable ParentFolderId { + get { + return this.parentFolderIdField; + } + set { + this.parentFolderIdField = value; + this.RaisePropertyChanged("ParentFolderId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=2)] + public System.Nullable InheritPermissions { + get { + return this.inheritPermissionsField; + } + set { + this.inheritPermissionsField = value; + this.RaisePropertyChanged("InheritPermissions"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FolderExtendedGetNewResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FolderExtendedUpdateResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FolderExtendedGetResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FolderExtendedCreateResult))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedResultBase : object, System.ComponentModel.INotifyPropertyChanged { + + private bool successField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedGetNewResult : FolderExtendedResultBase { + + private FolderExtended folderField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public FolderExtended Folder { + get { + return this.folderField; + } + set { + this.folderField = value; + this.RaisePropertyChanged("Folder"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedUpdateResult : FolderExtendedResultBase { + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedGetResult : FolderExtendedResultBase { + + private FolderExtended folderField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public FolderExtended Folder { + get { + return this.folderField; + } + set { + this.folderField = value; + this.RaisePropertyChanged("Folder"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FolderExtendedCreateResult : FolderExtendedResultBase { + + private int folderIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int FolderId { + get { + return this.folderIdField; + } + set { + this.folderIdField = value; + this.RaisePropertyChanged("FolderId"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetFoldersResult : object, System.ComponentModel.INotifyPropertyChanged { + + private Folder[] foldersField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public Folder[] Folders { + get { + return this.foldersField; + } + set { + this.foldersField = value; + this.RaisePropertyChanged("Folders"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetFolderResult : object, System.ComponentModel.INotifyPropertyChanged { + + private Folder folderField; + + private string[] errorsField; + + private bool successField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public Folder Folder { + get { + return this.folderField; + } + set { + this.folderField = value; + this.RaisePropertyChanged("Folder"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Success { + get { + return this.successField; + } + set { + this.successField = value; + this.RaisePropertyChanged("Success"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class VersionGetResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private string versionField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Version { + get { + return this.versionField; + } + set { + this.versionField = value; + this.RaisePropertyChanged("Version"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GeneratePasswordResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string generatedPasswordField; + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string GeneratedPassword { + get { + return this.generatedPasswordField; + } + set { + this.generatedPasswordField = value; + this.RaisePropertyChanged("GeneratedPassword"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretTemplate : object, System.ComponentModel.INotifyPropertyChanged { + + private int idField; + + private string nameField; + + private SecretField[] fieldsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=2)] + public SecretField[] Fields { + get { + return this.fieldsField; + } + set { + this.fieldsField = value; + this.RaisePropertyChanged("Fields"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretField : object, System.ComponentModel.INotifyPropertyChanged { + + private string displayNameField; + + private int idField; + + private bool isPasswordField; + + private bool isUrlField; + + private bool isNotesField; + + private bool isFileField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string DisplayName { + get { + return this.displayNameField; + } + set { + this.displayNameField = value; + this.RaisePropertyChanged("DisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool IsPassword { + get { + return this.isPasswordField; + } + set { + this.isPasswordField = value; + this.RaisePropertyChanged("IsPassword"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public bool IsUrl { + get { + return this.isUrlField; + } + set { + this.isUrlField = value; + this.RaisePropertyChanged("IsUrl"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public bool IsNotes { + get { + return this.isNotesField; + } + set { + this.isNotesField = value; + this.RaisePropertyChanged("IsNotes"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool IsFile { + get { + return this.isFileField; + } + set { + this.isFileField = value; + this.RaisePropertyChanged("IsFile"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSecretTemplatesResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private SecretTemplate[] secretTemplatesField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public SecretTemplate[] SecretTemplates { + get { + return this.secretTemplatesField; + } + set { + this.secretTemplatesField = value; + this.RaisePropertyChanged("SecretTemplates"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSecretTemplateFieldsResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private SecretField[] fieldsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public SecretField[] Fields { + get { + return this.fieldsField; + } + set { + this.fieldsField = value; + this.RaisePropertyChanged("Fields"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AddSecretResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private Secret secretField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public Secret Secret { + get { + return this.secretField; + } + set { + this.secretField = value; + this.RaisePropertyChanged("Secret"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Secret : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private SecretItem[] itemsField; + + private int idField; + + private int secretTypeIdField; + + private int folderIdField; + + private bool isWebLauncherField; + + private System.Nullable checkOutMinutesRemainingField; + + private System.Nullable isCheckedOutField; + + private string checkOutUserDisplayNameField; + + private System.Nullable checkOutUserIdField; + + private System.Nullable isOutOfSyncField; + + private System.Nullable isRestrictedField; + + private string outOfSyncReasonField; + + private SecretSettings secretSettingsField; + + private SecretPermissions secretPermissionsField; + + private System.Nullable activeField; + + private bool activeFieldSpecified; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public SecretItem[] Items { + get { + return this.itemsField; + } + set { + this.itemsField = value; + this.RaisePropertyChanged("Items"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public int SecretTypeId { + get { + return this.secretTypeIdField; + } + set { + this.secretTypeIdField = value; + this.RaisePropertyChanged("SecretTypeId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int FolderId { + get { + return this.folderIdField; + } + set { + this.folderIdField = value; + this.RaisePropertyChanged("FolderId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool IsWebLauncher { + get { + return this.isWebLauncherField; + } + set { + this.isWebLauncherField = value; + this.RaisePropertyChanged("IsWebLauncher"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=6)] + public System.Nullable CheckOutMinutesRemaining { + get { + return this.checkOutMinutesRemainingField; + } + set { + this.checkOutMinutesRemainingField = value; + this.RaisePropertyChanged("CheckOutMinutesRemaining"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=7)] + public System.Nullable IsCheckedOut { + get { + return this.isCheckedOutField; + } + set { + this.isCheckedOutField = value; + this.RaisePropertyChanged("IsCheckedOut"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public string CheckOutUserDisplayName { + get { + return this.checkOutUserDisplayNameField; + } + set { + this.checkOutUserDisplayNameField = value; + this.RaisePropertyChanged("CheckOutUserDisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=9)] + public System.Nullable CheckOutUserId { + get { + return this.checkOutUserIdField; + } + set { + this.checkOutUserIdField = value; + this.RaisePropertyChanged("CheckOutUserId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=10)] + public System.Nullable IsOutOfSync { + get { + return this.isOutOfSyncField; + } + set { + this.isOutOfSyncField = value; + this.RaisePropertyChanged("IsOutOfSync"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=11)] + public System.Nullable IsRestricted { + get { + return this.isRestrictedField; + } + set { + this.isRestrictedField = value; + this.RaisePropertyChanged("IsRestricted"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=12)] + public string OutOfSyncReason { + get { + return this.outOfSyncReasonField; + } + set { + this.outOfSyncReasonField = value; + this.RaisePropertyChanged("OutOfSyncReason"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=13)] + public SecretSettings SecretSettings { + get { + return this.secretSettingsField; + } + set { + this.secretSettingsField = value; + this.RaisePropertyChanged("SecretSettings"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=14)] + public SecretPermissions SecretPermissions { + get { + return this.secretPermissionsField; + } + set { + this.secretPermissionsField = value; + this.RaisePropertyChanged("SecretPermissions"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=15)] + public System.Nullable Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool ActiveSpecified { + get { + return this.activeFieldSpecified; + } + set { + this.activeFieldSpecified = value; + this.RaisePropertyChanged("ActiveSpecified"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretItem : object, System.ComponentModel.INotifyPropertyChanged { + + private string valueField; + + private System.Nullable idField; + + private System.Nullable fieldIdField; + + private string fieldNameField; + + private bool isFileField; + + private bool isNotesField; + + private bool isPasswordField; + + private string fieldDisplayNameField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=2)] + public System.Nullable FieldId { + get { + return this.fieldIdField; + } + set { + this.fieldIdField = value; + this.RaisePropertyChanged("FieldId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string FieldName { + get { + return this.fieldNameField; + } + set { + this.fieldNameField = value; + this.RaisePropertyChanged("FieldName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public bool IsFile { + get { + return this.isFileField; + } + set { + this.isFileField = value; + this.RaisePropertyChanged("IsFile"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool IsNotes { + get { + return this.isNotesField; + } + set { + this.isNotesField = value; + this.RaisePropertyChanged("IsNotes"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public bool IsPassword { + get { + return this.isPasswordField; + } + set { + this.isPasswordField = value; + this.RaisePropertyChanged("IsPassword"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public string FieldDisplayName { + get { + return this.fieldDisplayNameField; + } + set { + this.fieldDisplayNameField = value; + this.RaisePropertyChanged("FieldDisplayName"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretSettings : object, System.ComponentModel.INotifyPropertyChanged { + + private System.Nullable autoChangeEnabledField; + + private System.Nullable requiresApprovalForAccessField; + + private System.Nullable requiresCommentField; + + private System.Nullable checkOutEnabledField; + + private System.Nullable checkOutChangePasswordEnabledField; + + private System.Nullable proxyEnabledField; + + private System.Nullable sessionRecordingEnabledField; + + private System.Nullable restrictSshCommandsField; + + private System.Nullable allowOwnersUnrestrictedSshCommandsField; + + private System.Nullable privilegedSecretIdField; + + private int[] associatedSecretIdsField; + + private GroupOrUserRecord[] approversField; + + private SshCommandMenuAccessPermission[] sshCommandMenuAccessPermissionsField; + + private bool isChangeToSettingsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=0)] + public System.Nullable AutoChangeEnabled { + get { + return this.autoChangeEnabledField; + } + set { + this.autoChangeEnabledField = value; + this.RaisePropertyChanged("AutoChangeEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable RequiresApprovalForAccess { + get { + return this.requiresApprovalForAccessField; + } + set { + this.requiresApprovalForAccessField = value; + this.RaisePropertyChanged("RequiresApprovalForAccess"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=2)] + public System.Nullable RequiresComment { + get { + return this.requiresCommentField; + } + set { + this.requiresCommentField = value; + this.RaisePropertyChanged("RequiresComment"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable CheckOutEnabled { + get { + return this.checkOutEnabledField; + } + set { + this.checkOutEnabledField = value; + this.RaisePropertyChanged("CheckOutEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=4)] + public System.Nullable CheckOutChangePasswordEnabled { + get { + return this.checkOutChangePasswordEnabledField; + } + set { + this.checkOutChangePasswordEnabledField = value; + this.RaisePropertyChanged("CheckOutChangePasswordEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=5)] + public System.Nullable ProxyEnabled { + get { + return this.proxyEnabledField; + } + set { + this.proxyEnabledField = value; + this.RaisePropertyChanged("ProxyEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=6)] + public System.Nullable SessionRecordingEnabled { + get { + return this.sessionRecordingEnabledField; + } + set { + this.sessionRecordingEnabledField = value; + this.RaisePropertyChanged("SessionRecordingEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=7)] + public System.Nullable RestrictSshCommands { + get { + return this.restrictSshCommandsField; + } + set { + this.restrictSshCommandsField = value; + this.RaisePropertyChanged("RestrictSshCommands"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=8)] + public System.Nullable AllowOwnersUnrestrictedSshCommands { + get { + return this.allowOwnersUnrestrictedSshCommandsField; + } + set { + this.allowOwnersUnrestrictedSshCommandsField = value; + this.RaisePropertyChanged("AllowOwnersUnrestrictedSshCommands"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=9)] + public System.Nullable PrivilegedSecretId { + get { + return this.privilegedSecretIdField; + } + set { + this.privilegedSecretIdField = value; + this.RaisePropertyChanged("PrivilegedSecretId"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=10)] + public int[] AssociatedSecretIds { + get { + return this.associatedSecretIdsField; + } + set { + this.associatedSecretIdsField = value; + this.RaisePropertyChanged("AssociatedSecretIds"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=11)] + public GroupOrUserRecord[] Approvers { + get { + return this.approversField; + } + set { + this.approversField = value; + this.RaisePropertyChanged("Approvers"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=12)] + public SshCommandMenuAccessPermission[] SshCommandMenuAccessPermissions { + get { + return this.sshCommandMenuAccessPermissionsField; + } + set { + this.sshCommandMenuAccessPermissionsField = value; + this.RaisePropertyChanged("SshCommandMenuAccessPermissions"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=13)] + public bool IsChangeToSettings { + get { + return this.isChangeToSettingsField; + } + set { + this.isChangeToSettingsField = value; + this.RaisePropertyChanged("IsChangeToSettings"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshCommandMenuAccessPermission : object, System.ComponentModel.INotifyPropertyChanged { + + private GroupOrUserRecord groupOrUserRecordField; + + private int secretIdField; + + private string concurrencyIdField; + + private string displayNameField; + + private string sshCommandMenuNameField; + + private bool isUnrestrictedField; + + private System.Nullable sshCommandMenuIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public GroupOrUserRecord GroupOrUserRecord { + get { + return this.groupOrUserRecordField; + } + set { + this.groupOrUserRecordField = value; + this.RaisePropertyChanged("GroupOrUserRecord"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string ConcurrencyId { + get { + return this.concurrencyIdField; + } + set { + this.concurrencyIdField = value; + this.RaisePropertyChanged("ConcurrencyId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string DisplayName { + get { + return this.displayNameField; + } + set { + this.displayNameField = value; + this.RaisePropertyChanged("DisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string SshCommandMenuName { + get { + return this.sshCommandMenuNameField; + } + set { + this.sshCommandMenuNameField = value; + this.RaisePropertyChanged("SshCommandMenuName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool IsUnrestricted { + get { + return this.isUnrestrictedField; + } + set { + this.isUnrestrictedField = value; + this.RaisePropertyChanged("IsUnrestricted"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=6)] + public System.Nullable SshCommandMenuId { + get { + return this.sshCommandMenuIdField; + } + set { + this.sshCommandMenuIdField = value; + this.RaisePropertyChanged("SshCommandMenuId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPermissions : object, System.ComponentModel.INotifyPropertyChanged { + + private bool currentUserHasViewField; + + private bool currentUserHasEditField; + + private bool currentUserHasOwnerField; + + private System.Nullable inheritPermissionsEnabledField; + + private bool isChangeToPermissionsField; + + private Permission[] permissionsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public bool CurrentUserHasView { + get { + return this.currentUserHasViewField; + } + set { + this.currentUserHasViewField = value; + this.RaisePropertyChanged("CurrentUserHasView"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public bool CurrentUserHasEdit { + get { + return this.currentUserHasEditField; + } + set { + this.currentUserHasEditField = value; + this.RaisePropertyChanged("CurrentUserHasEdit"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool CurrentUserHasOwner { + get { + return this.currentUserHasOwnerField; + } + set { + this.currentUserHasOwnerField = value; + this.RaisePropertyChanged("CurrentUserHasOwner"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable InheritPermissionsEnabled { + get { + return this.inheritPermissionsEnabledField; + } + set { + this.inheritPermissionsEnabledField = value; + this.RaisePropertyChanged("InheritPermissionsEnabled"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public bool IsChangeToPermissions { + get { + return this.isChangeToPermissionsField; + } + set { + this.isChangeToPermissionsField = value; + this.RaisePropertyChanged("IsChangeToPermissions"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=5)] + public Permission[] Permissions { + get { + return this.permissionsField; + } + set { + this.permissionsField = value; + this.RaisePropertyChanged("Permissions"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class Permission : object, System.ComponentModel.INotifyPropertyChanged { + + private GroupOrUserRecord userOrGroupField; + + private bool viewField; + + private bool editField; + + private bool ownerField; + + private string secretAccessRoleNameField; + + private System.Nullable secretAccessRoleIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public GroupOrUserRecord UserOrGroup { + get { + return this.userOrGroupField; + } + set { + this.userOrGroupField = value; + this.RaisePropertyChanged("UserOrGroup"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public bool View { + get { + return this.viewField; + } + set { + this.viewField = value; + this.RaisePropertyChanged("View"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Edit { + get { + return this.editField; + } + set { + this.editField = value; + this.RaisePropertyChanged("Edit"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public bool Owner { + get { + return this.ownerField; + } + set { + this.ownerField = value; + this.RaisePropertyChanged("Owner"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string SecretAccessRoleName { + get { + return this.secretAccessRoleNameField; + } + set { + this.secretAccessRoleNameField = value; + this.RaisePropertyChanged("SecretAccessRoleName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=5)] + public System.Nullable SecretAccessRoleId { + get { + return this.secretAccessRoleIdField; + } + set { + this.secretAccessRoleIdField = value; + this.RaisePropertyChanged("SecretAccessRoleId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetFavoritesResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private SecretSummary[] secretSummariesField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public SecretSummary[] SecretSummaries { + get { + return this.secretSummariesField; + } + set { + this.secretSummariesField = value; + this.RaisePropertyChanged("SecretSummaries"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretSummary : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretIdField; + + private string secretNameField; + + private string secretTypeNameField; + + private int secretTypeIdField; + + private int folderIdField; + + private bool isRestrictedField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string SecretName { + get { + return this.secretNameField; + } + set { + this.secretNameField = value; + this.RaisePropertyChanged("SecretName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string SecretTypeName { + get { + return this.secretTypeNameField; + } + set { + this.secretTypeNameField = value; + this.RaisePropertyChanged("SecretTypeName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public int SecretTypeId { + get { + return this.secretTypeIdField; + } + set { + this.secretTypeIdField = value; + this.RaisePropertyChanged("SecretTypeId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public int FolderId { + get { + return this.folderIdField; + } + set { + this.folderIdField = value; + this.RaisePropertyChanged("FolderId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public bool IsRestricted { + get { + return this.isRestrictedField; + } + set { + this.isRestrictedField = value; + this.RaisePropertyChanged("IsRestricted"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SearchSecretsResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private SecretSummary[] secretSummariesField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public SecretSummary[] SecretSummaries { + get { + return this.secretSummariesField; + } + set { + this.secretSummariesField = value; + this.RaisePropertyChanged("SecretSummaries"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSecretsByFieldValueResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private Secret[] secretsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=1)] + public Secret[] Secrets { + get { + return this.secretsField; + } + set { + this.secretsField = value; + this.RaisePropertyChanged("Secrets"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SqlScriptArgument2 : object, System.ComponentModel.INotifyPropertyChanged { + + private string nameField; + + private object valueField; + + private DbType dbTypeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public object Value { + get { + return this.valueField; + } + set { + this.valueField = value; + this.RaisePropertyChanged("Value"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public DbType DbType { + get { + return this.dbTypeField; + } + set { + this.dbTypeField = value; + this.RaisePropertyChanged("DbType"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AdditionalDataSqlObject : object, System.ComponentModel.INotifyPropertyChanged { + + private SqlScriptArgument2[] paramsField; + + private int passwordChangerIdField; + + private int versionField; + + private string databaseField; + + private string portField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public SqlScriptArgument2[] Params { + get { + return this.paramsField; + } + set { + this.paramsField = value; + this.RaisePropertyChanged("Params"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int PasswordChangerId { + get { + return this.passwordChangerIdField; + } + set { + this.passwordChangerIdField = value; + this.RaisePropertyChanged("PasswordChangerId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int Version { + get { + return this.versionField; + } + set { + this.versionField = value; + this.RaisePropertyChanged("Version"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string Database { + get { + return this.databaseField; + } + set { + this.databaseField = value; + this.RaisePropertyChanged("Database"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(PowerShellUserScript))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SshUserScript))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SqlUserScript))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public abstract partial class UserScript : object, System.ComponentModel.INotifyPropertyChanged { + + private int scriptIdField; + + private string nameField; + + private string descriptionField; + + private string scriptField; + + private bool activeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int ScriptId { + get { + return this.scriptIdField; + } + set { + this.scriptIdField = value; + this.RaisePropertyChanged("ScriptId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Description { + get { + return this.descriptionField; + } + set { + this.descriptionField = value; + this.RaisePropertyChanged("Description"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string Script { + get { + return this.scriptField; + } + set { + this.scriptField = value; + this.RaisePropertyChanged("Script"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class PowerShellUserScript : UserScript { + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshUserScript : UserScript { + + private AdditionalDataSshObject additionalDataObjectField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public AdditionalDataSshObject AdditionalDataObject { + get { + return this.additionalDataObjectField; + } + set { + this.additionalDataObjectField = value; + this.RaisePropertyChanged("AdditionalDataObject"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SqlUserScript : UserScript { + + private AdditionalDataSqlObject additionalDataObjectField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public AdditionalDataSqlObject AdditionalDataObject { + get { + return this.additionalDataObjectField; + } + set { + this.additionalDataObjectField = value; + this.RaisePropertyChanged("AdditionalDataObject"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SshCommandMenuGroupMap : object, System.ComponentModel.INotifyPropertyChanged { + + private System.Nullable sshCommandMenuIdField; + + private UserGroupMap userGroupMapField; + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=0)] + public System.Nullable SshCommandMenuId { + get { + return this.sshCommandMenuIdField; + } + set { + this.sshCommandMenuIdField = value; + this.RaisePropertyChanged("SshCommandMenuId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public UserGroupMap UserGroupMap { + get { + return this.userGroupMapField; + } + set { + this.userGroupMapField = value; + this.RaisePropertyChanged("UserGroupMap"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class UserGroupMap : object, System.ComponentModel.INotifyPropertyChanged { + + private int idField; + + private UserGroupMapType userGroupMapTypeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int Id { + get { + return this.idField; + } + set { + this.idField = value; + this.RaisePropertyChanged("Id"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public UserGroupMapType UserGroupMapType { + get { + return this.userGroupMapTypeField; + } + set { + this.userGroupMapTypeField = value; + this.RaisePropertyChanged("UserGroupMapType"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public enum UserGroupMapType { + + /// + User, + + /// + Group, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicyItem : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretPolicyItemMapIdField; + + private int secretPolicyItemIdField; + + private string policyApplyCodeField; + + private System.Nullable enabledValueField; + + private System.Nullable integerValueField; + + private System.Nullable secretIdField; + + private string stringValueField; + + private string nameField; + + private string descriptionField; + + private string valueTypeField; + + private System.Nullable parentSecretPolicyItemIdField; + + private string sectionNameField; + + private UserGroupMap[] userGroupMapsField; + + private SshCommandMenuGroupMap[] sshCommandMenuGroupMapsField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretPolicyItemMapId { + get { + return this.secretPolicyItemMapIdField; + } + set { + this.secretPolicyItemMapIdField = value; + this.RaisePropertyChanged("SecretPolicyItemMapId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int SecretPolicyItemId { + get { + return this.secretPolicyItemIdField; + } + set { + this.secretPolicyItemIdField = value; + this.RaisePropertyChanged("SecretPolicyItemId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string PolicyApplyCode { + get { + return this.policyApplyCodeField; + } + set { + this.policyApplyCodeField = value; + this.RaisePropertyChanged("PolicyApplyCode"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable EnabledValue { + get { + return this.enabledValueField; + } + set { + this.enabledValueField = value; + this.RaisePropertyChanged("EnabledValue"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=4)] + public System.Nullable IntegerValue { + get { + return this.integerValueField; + } + set { + this.integerValueField = value; + this.RaisePropertyChanged("IntegerValue"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=5)] + public System.Nullable SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=6)] + public string StringValue { + get { + return this.stringValueField; + } + set { + this.stringValueField = value; + this.RaisePropertyChanged("StringValue"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=7)] + public string Name { + get { + return this.nameField; + } + set { + this.nameField = value; + this.RaisePropertyChanged("Name"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=8)] + public string Description { + get { + return this.descriptionField; + } + set { + this.descriptionField = value; + this.RaisePropertyChanged("Description"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=9)] + public string ValueType { + get { + return this.valueTypeField; + } + set { + this.valueTypeField = value; + this.RaisePropertyChanged("ValueType"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=10)] + public System.Nullable ParentSecretPolicyItemId { + get { + return this.parentSecretPolicyItemIdField; + } + set { + this.parentSecretPolicyItemIdField = value; + this.RaisePropertyChanged("ParentSecretPolicyItemId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=11)] + public string SectionName { + get { + return this.sectionNameField; + } + set { + this.sectionNameField = value; + this.RaisePropertyChanged("SectionName"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=12)] + public UserGroupMap[] UserGroupMaps { + get { + return this.userGroupMapsField; + } + set { + this.userGroupMapsField = value; + this.RaisePropertyChanged("UserGroupMaps"); + } + } + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=13)] + public SshCommandMenuGroupMap[] SshCommandMenuGroupMaps { + get { + return this.sshCommandMenuGroupMapsField; + } + set { + this.sshCommandMenuGroupMapsField = value; + this.RaisePropertyChanged("SshCommandMenuGroupMaps"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SecretPolicyDetail))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicySummary : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretPolicyIdField; + + private string secretPolicyNameField; + + private string secretPolicyDescriptionField; + + private bool activeField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretPolicyId { + get { + return this.secretPolicyIdField; + } + set { + this.secretPolicyIdField = value; + this.RaisePropertyChanged("SecretPolicyId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string SecretPolicyName { + get { + return this.secretPolicyNameField; + } + set { + this.secretPolicyNameField = value; + this.RaisePropertyChanged("SecretPolicyName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string SecretPolicyDescription { + get { + return this.secretPolicyDescriptionField; + } + set { + this.secretPolicyDescriptionField = value; + this.RaisePropertyChanged("SecretPolicyDescription"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public bool Active { + get { + return this.activeField; + } + set { + this.activeField = value; + this.RaisePropertyChanged("Active"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicyDetail : SecretPolicySummary { + + private SecretPolicyItem[] secretPolicyItemsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public SecretPolicyItem[] SecretPolicyItems { + get { + return this.secretPolicyItemsField; + } + set { + this.secretPolicyItemsField = value; + this.RaisePropertyChanged("SecretPolicyItems"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicyForSecret : object, System.ComponentModel.INotifyPropertyChanged { + + private int secretIdField; + + private System.Nullable secretPolicyIdField; + + private bool inheritField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int SecretId { + get { + return this.secretIdField; + } + set { + this.secretIdField = value; + this.RaisePropertyChanged("SecretId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=1)] + public System.Nullable SecretPolicyId { + get { + return this.secretPolicyIdField; + } + set { + this.secretPolicyIdField = value; + this.RaisePropertyChanged("SecretPolicyId"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool Inherit { + get { + return this.inheritField; + } + set { + this.inheritField = value; + this.RaisePropertyChanged("Inherit"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(UpdateUserScriptResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(GetUserScriptResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(GetUserScriptsResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SecretPolicyResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SearchSecretPoliciesResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SecretPolicyForSecretResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(SSHCredentialsResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(FileDownloadResult))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(CreateFolderResult))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class WebServiceResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class UpdateUserScriptResult : WebServiceResult { + + private UserScript userScriptField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public UserScript UserScript { + get { + return this.userScriptField; + } + set { + this.userScriptField = value; + this.RaisePropertyChanged("UserScript"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetUserScriptResult : WebServiceResult { + + private UserScript userScriptField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public UserScript UserScript { + get { + return this.userScriptField; + } + set { + this.userScriptField = value; + this.RaisePropertyChanged("UserScript"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetUserScriptsResult : WebServiceResult { + + private UserScript[] userScriptsField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public UserScript[] UserScripts { + get { + return this.userScriptsField; + } + set { + this.userScriptsField = value; + this.RaisePropertyChanged("UserScripts"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicyResult : WebServiceResult { + + private SecretPolicyDetail secretPolicyField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public SecretPolicyDetail SecretPolicy { + get { + return this.secretPolicyField; + } + set { + this.secretPolicyField = value; + this.RaisePropertyChanged("SecretPolicy"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SearchSecretPoliciesResult : WebServiceResult { + + private SecretPolicySummary[] secretPoliciesField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public SecretPolicySummary[] SecretPolicies { + get { + return this.secretPoliciesField; + } + set { + this.secretPoliciesField = value; + this.RaisePropertyChanged("SecretPolicies"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretPolicyForSecretResult : WebServiceResult { + + private SecretPolicyForSecret secretPolicyForSecretField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public SecretPolicyForSecret SecretPolicyForSecret { + get { + return this.secretPolicyForSecretField; + } + set { + this.secretPolicyForSecretField = value; + this.RaisePropertyChanged("SecretPolicyForSecret"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SSHCredentialsResult : WebServiceResult { + + private string usernameField; + + private string passwordField; + + private string hostField; + + private string portField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string Username { + get { + return this.usernameField; + } + set { + this.usernameField = value; + this.RaisePropertyChanged("Username"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Password { + get { + return this.passwordField; + } + set { + this.passwordField = value; + this.RaisePropertyChanged("Password"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Host { + get { + return this.hostField; + } + set { + this.hostField = value; + this.RaisePropertyChanged("Host"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string Port { + get { + return this.portField; + } + set { + this.portField = value; + this.RaisePropertyChanged("Port"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class FileDownloadResult : WebServiceResult { + + private byte[] fileAttachmentField; + + private string fileNameField; + + /// + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary", Order=0)] + public byte[] FileAttachment { + get { + return this.fileAttachmentField; + } + set { + this.fileAttachmentField = value; + this.RaisePropertyChanged("FileAttachment"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string FileName { + get { + return this.fileNameField; + } + set { + this.fileNameField = value; + this.RaisePropertyChanged("FileName"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class CreateFolderResult : WebServiceResult { + + private int folderIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public int FolderId { + get { + return this.folderIdField; + } + set { + this.folderIdField = value; + this.RaisePropertyChanged("FolderId"); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetCheckOutStatusResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private Secret secretField; + + private int checkOutMinutesRemainingField; + + private bool isCheckedOutField; + + private string checkOutUserDisplayNameField; + + private int checkOutUserIdField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public Secret Secret { + get { + return this.secretField; + } + set { + this.secretField = value; + this.RaisePropertyChanged("Secret"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public int CheckOutMinutesRemaining { + get { + return this.checkOutMinutesRemainingField; + } + set { + this.checkOutMinutesRemainingField = value; + this.RaisePropertyChanged("CheckOutMinutesRemaining"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public bool IsCheckedOut { + get { + return this.isCheckedOutField; + } + set { + this.isCheckedOutField = value; + this.RaisePropertyChanged("IsCheckedOut"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string CheckOutUserDisplayName { + get { + return this.checkOutUserDisplayNameField; + } + set { + this.checkOutUserDisplayNameField = value; + this.RaisePropertyChanged("CheckOutUserDisplayName"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=5)] + public int CheckOutUserId { + get { + return this.checkOutUserIdField; + } + set { + this.checkOutUserIdField = value; + this.RaisePropertyChanged("CheckOutUserId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class CodeResponse : object, System.ComponentModel.INotifyPropertyChanged { + + private string errorCodeField; + + private string commentField; + + private string additionalCommentField; + + private System.Nullable ticketSystemIdField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string ErrorCode { + get { + return this.errorCodeField; + } + set { + this.errorCodeField = value; + this.RaisePropertyChanged("ErrorCode"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Comment { + get { + return this.commentField; + } + set { + this.commentField = value; + this.RaisePropertyChanged("Comment"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string AdditionalComment { + get { + return this.additionalCommentField; + } + set { + this.additionalCommentField = value; + this.RaisePropertyChanged("AdditionalComment"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true, Order=3)] + public System.Nullable TicketSystemId { + get { + return this.ticketSystemIdField; + } + set { + this.ticketSystemIdField = value; + this.RaisePropertyChanged("TicketSystemId"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class SecretError : object, System.ComponentModel.INotifyPropertyChanged { + + private string errorCodeField; + + private string errorMessageField; + + private bool allowsResponseField; + + private string commentTitleField; + + private string additionalCommentTitleField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string ErrorCode { + get { + return this.errorCodeField; + } + set { + this.errorCodeField = value; + this.RaisePropertyChanged("ErrorCode"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string ErrorMessage { + get { + return this.errorMessageField; + } + set { + this.errorMessageField = value; + this.RaisePropertyChanged("ErrorMessage"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public bool AllowsResponse { + get { + return this.allowsResponseField; + } + set { + this.allowsResponseField = value; + this.RaisePropertyChanged("AllowsResponse"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=3)] + public string CommentTitle { + get { + return this.commentTitleField; + } + set { + this.commentTitleField = value; + this.RaisePropertyChanged("CommentTitle"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=4)] + public string AdditionalCommentTitle { + get { + return this.additionalCommentTitleField; + } + set { + this.additionalCommentTitleField = value; + this.RaisePropertyChanged("AdditionalCommentTitle"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GetSecretResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private SecretError secretErrorField; + + private Secret secretField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public SecretError SecretError { + get { + return this.secretErrorField; + } + set { + this.secretErrorField = value; + this.RaisePropertyChanged("SecretError"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public Secret Secret { + get { + return this.secretField; + } + set { + this.secretField = value; + this.RaisePropertyChanged("Secret"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class TokenIsValidResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private int maxOfflineSecondsField; + + private string versionField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public int MaxOfflineSeconds { + get { + return this.maxOfflineSecondsField; + } + set { + this.maxOfflineSecondsField = value; + this.RaisePropertyChanged("MaxOfflineSeconds"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string Version { + get { + return this.versionField; + } + set { + this.versionField = value; + this.RaisePropertyChanged("Version"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class ImpersonateResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private string tokenField; + + private string authorizeURLField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Token { + get { + return this.tokenField; + } + set { + this.tokenField = value; + this.RaisePropertyChanged("Token"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=2)] + public string AuthorizeURL { + get { + return this.authorizeURLField; + } + set { + this.authorizeURLField = value; + this.RaisePropertyChanged("AuthorizeURL"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class AuthenticateResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string[] errorsField; + + private string tokenField; + + /// + [System.Xml.Serialization.XmlArrayAttribute(Order=0)] + public string[] Errors { + get { + return this.errorsField; + } + set { + this.errorsField = value; + this.RaisePropertyChanged("Errors"); + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=1)] + public string Token { + get { + return this.tokenField; + } + set { + this.tokenField = value; + this.RaisePropertyChanged("Token"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + [System.Xml.Serialization.XmlIncludeAttribute(typeof(RequestApprovalResult))] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1590.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:thesecretserver.com")] + public partial class GenericResult : object, System.ComponentModel.INotifyPropertyChanged { + + private string errorMessageField; + + /// + [System.Xml.Serialization.XmlElementAttribute(Order=0)] + public string ErrorMessage { + get { + return this.errorMessageField; + } + set { + this.errorMessageField = value; + this.RaisePropertyChanged("ErrorMessage"); + } + } + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + protected void RaisePropertyChanged(string propertyName) { + System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; + if ((propertyChanged != null)) { + propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="GetSecret", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class GetSecretRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public int secretId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable loadSettingsAndPermissions; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public Alkami.Ops.SecretServer.SSWebService.CodeResponse[] codeResponses; + + public GetSecretRequest() { + } + + public GetSecretRequest(string token, int secretId, System.Nullable loadSettingsAndPermissions, Alkami.Ops.SecretServer.SSWebService.CodeResponse[] codeResponses) { + this.token = token; + this.secretId = secretId; + this.loadSettingsAndPermissions = loadSettingsAndPermissions; + this.codeResponses = codeResponses; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="GetSecretResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class GetSecretResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetSecretResult; + + public GetSecretResponse() { + } + + public GetSecretResponse(Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetSecretResult) { + this.GetSecretResult = GetSecretResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecrets", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public string searchTerm; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable includeDeleted; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable includeRestricted; + + public SearchSecretsRequest() { + } + + public SearchSecretsRequest(string token, string searchTerm, System.Nullable includeDeleted, System.Nullable includeRestricted) { + this.token = token; + this.searchTerm = searchTerm; + this.includeDeleted = includeDeleted; + this.includeRestricted = includeRestricted; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecretsResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsResult; + + public SearchSecretsResponse() { + } + + public SearchSecretsResponse(Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsResult) { + this.SearchSecretsResult = SearchSecretsResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecretsByFolder", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsByFolderRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public string searchTerm; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable folderId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public bool includeSubFolders; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=4)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable includeDeleted; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=5)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable includeRestricted; + + public SearchSecretsByFolderRequest() { + } + + public SearchSecretsByFolderRequest(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders, System.Nullable includeDeleted, System.Nullable includeRestricted) { + this.token = token; + this.searchTerm = searchTerm; + this.folderId = folderId; + this.includeSubFolders = includeSubFolders; + this.includeDeleted = includeDeleted; + this.includeRestricted = includeRestricted; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecretsByFolderResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsByFolderResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolderResult; + + public SearchSecretsByFolderResponse() { + } + + public SearchSecretsByFolderResponse(Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolderResult) { + this.SearchSecretsByFolderResult = SearchSecretsByFolderResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecretsByFolderLegacy", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsByFolderLegacyRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public string searchTerm; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable folderId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public bool includeSubFolders; + + public SearchSecretsByFolderLegacyRequest() { + } + + public SearchSecretsByFolderLegacyRequest(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders) { + this.token = token; + this.searchTerm = searchTerm; + this.folderId = folderId; + this.includeSubFolders = includeSubFolders; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SearchSecretsByFolderLegacyResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SearchSecretsByFolderLegacyResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolderLegacyResult; + + public SearchSecretsByFolderLegacyResponse() { + } + + public SearchSecretsByFolderLegacyResponse(Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolderLegacyResult) { + this.SearchSecretsByFolderLegacyResult = SearchSecretsByFolderLegacyResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="UploadFileAttachment", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class UploadFileAttachmentRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public int secretId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] + public byte[] fileData; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public string fileName; + + public UploadFileAttachmentRequest() { + } + + public UploadFileAttachmentRequest(string token, int secretId, byte[] fileData, string fileName) { + this.token = token; + this.secretId = secretId; + this.fileData = fileData; + this.fileName = fileName; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="UploadFileAttachmentResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class UploadFileAttachmentResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachmentResult; + + public UploadFileAttachmentResponse() { + } + + public UploadFileAttachmentResponse(Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachmentResult) { + this.UploadFileAttachmentResult = UploadFileAttachmentResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="UploadFileAttachmentByItemId", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class UploadFileAttachmentByItemIdRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public int secretId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + public int secretItemId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] + public byte[] fileData; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=4)] + public string fileName; + + public UploadFileAttachmentByItemIdRequest() { + } + + public UploadFileAttachmentByItemIdRequest(string token, int secretId, int secretItemId, byte[] fileData, string fileName) { + this.token = token; + this.secretId = secretId; + this.secretItemId = secretItemId; + this.fileData = fileData; + this.fileName = fileName; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="UploadFileAttachmentByItemIdResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class UploadFileAttachmentByItemIdResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachmentByItemIdResult; + + public UploadFileAttachmentByItemIdResponse() { + } + + public UploadFileAttachmentByItemIdResponse(Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachmentByItemIdResult) { + this.UploadFileAttachmentByItemIdResult = UploadFileAttachmentByItemIdResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SetCheckOutEnabled", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SetCheckOutEnabledRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public int secretId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + public bool setCheckOut; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public bool setPasswordChangeOnCheckIn; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=4)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable checkOutInterval; + + public SetCheckOutEnabledRequest() { + } + + public SetCheckOutEnabledRequest(string token, int secretId, bool setCheckOut, bool setPasswordChangeOnCheckIn, System.Nullable checkOutInterval) { + this.token = token; + this.secretId = secretId; + this.setCheckOut = setCheckOut; + this.setPasswordChangeOnCheckIn = setPasswordChangeOnCheckIn; + this.checkOutInterval = checkOutInterval; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="SetCheckOutEnabledResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class SetCheckOutEnabledResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult SetCheckOutEnabledResult; + + public SetCheckOutEnabledResponse() { + } + + public SetCheckOutEnabledResponse(Alkami.Ops.SecretServer.SSWebService.WebServiceResult SetCheckOutEnabledResult) { + this.SetCheckOutEnabledResult = SetCheckOutEnabledResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="AddSecretCustomAudit", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class AddSecretCustomAuditRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + public int secretId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=2)] + public string notes; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=3)] + public string ipAddress; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=4)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable referenceId; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=5)] + public string ticketNumber; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=6)] + public int userId; + + public AddSecretCustomAuditRequest() { + } + + public AddSecretCustomAuditRequest(string token, int secretId, string notes, string ipAddress, System.Nullable referenceId, string ticketNumber, int userId) { + this.token = token; + this.secretId = secretId; + this.notes = notes; + this.ipAddress = ipAddress; + this.referenceId = referenceId; + this.ticketNumber = ticketNumber; + this.userId = userId; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="AddSecretCustomAuditResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class AddSecretCustomAuditResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddSecretCustomAuditResult; + + public AddSecretCustomAuditResponse() { + } + + public AddSecretCustomAuditResponse(Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddSecretCustomAuditResult) { + this.AddSecretCustomAuditResult = AddSecretCustomAuditResult; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="GetAllSSHCommandMenus", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class GetAllSSHCommandMenusRequest { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public string token; + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=1)] + [System.Xml.Serialization.XmlElementAttribute(IsNullable=true)] + public System.Nullable includeInactive; + + public GetAllSSHCommandMenusRequest() { + } + + public GetAllSSHCommandMenusRequest(string token, System.Nullable includeInactive) { + this.token = token; + this.includeInactive = includeInactive; + } + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + [System.ServiceModel.MessageContractAttribute(WrapperName="GetAllSSHCommandMenusResponse", WrapperNamespace="urn:thesecretserver.com", IsWrapped=true)] + public partial class GetAllSSHCommandMenusResponse { + + [System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:thesecretserver.com", Order=0)] + public Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenusResult GetAllSSHCommandMenusResult; + + public GetAllSSHCommandMenusResponse() { + } + + public GetAllSSHCommandMenusResponse(Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenusResult GetAllSSHCommandMenusResult) { + this.GetAllSSHCommandMenusResult = GetAllSSHCommandMenusResult; + } + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + public interface SSWebServiceSoapChannel : Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap, System.ServiceModel.IClientChannel { + } + + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] + public partial class SSWebServiceSoapClient : System.ServiceModel.ClientBase, Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap { + + public SSWebServiceSoapClient() { + } + + public SSWebServiceSoapClient(string endpointConfigurationName) : + base(endpointConfigurationName) { + } + + public SSWebServiceSoapClient(string endpointConfigurationName, string remoteAddress) : + base(endpointConfigurationName, remoteAddress) { + } + + public SSWebServiceSoapClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : + base(endpointConfigurationName, remoteAddress) { + } + + public SSWebServiceSoapClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : + base(binding, remoteAddress) { + } + + public Alkami.Ops.SecretServer.SSWebService.RequestApprovalResult ApproveSecretAccessRequest(string approvalId, string hours, bool userOverride) { + return base.Channel.ApproveSecretAccessRequest(approvalId, hours, userOverride); + } + + public System.Threading.Tasks.Task ApproveSecretAccessRequestAsync(string approvalId, string hours, bool userOverride) { + return base.Channel.ApproveSecretAccessRequestAsync(approvalId, hours, userOverride); + } + + public Alkami.Ops.SecretServer.SSWebService.RequestApprovalResult DenySecretAccessRequest(string approvalId, bool userOverride) { + return base.Channel.DenySecretAccessRequest(approvalId, userOverride); + } + + public System.Threading.Tasks.Task DenySecretAccessRequestAsync(string approvalId, bool userOverride) { + return base.Channel.DenySecretAccessRequestAsync(approvalId, userOverride); + } + + public Alkami.Ops.SecretServer.SSWebService.AuthenticateResult Authenticate(string username, string password, string organization, string domain) { + return base.Channel.Authenticate(username, password, organization, domain); + } + + public System.Threading.Tasks.Task AuthenticateAsync(string username, string password, string organization, string domain) { + return base.Channel.AuthenticateAsync(username, password, organization, domain); + } + + public Alkami.Ops.SecretServer.SSWebService.ImpersonateResult ImpersonateUser(string token, string username, string organization, string domain) { + return base.Channel.ImpersonateUser(token, username, organization, domain); + } + + public System.Threading.Tasks.Task ImpersonateUserAsync(string token, string username, string organization, string domain) { + return base.Channel.ImpersonateUserAsync(token, username, organization, domain); + } + + public Alkami.Ops.SecretServer.SSWebService.AuthenticateResult AuthenticateRADIUS(string username, string password, string organization, string domain, string radiusPassword) { + return base.Channel.AuthenticateRADIUS(username, password, organization, domain, radiusPassword); + } + + public System.Threading.Tasks.Task AuthenticateRADIUSAsync(string username, string password, string organization, string domain, string radiusPassword) { + return base.Channel.AuthenticateRADIUSAsync(username, password, organization, domain, radiusPassword); + } + + public Alkami.Ops.SecretServer.SSWebService.TokenIsValidResult GetTokenIsValid(string token) { + return base.Channel.GetTokenIsValid(token); + } + + public System.Threading.Tasks.Task GetTokenIsValidAsync(string token) { + return base.Channel.GetTokenIsValidAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetSecretLegacy(string token, int secretId) { + return base.Channel.GetSecretLegacy(token, secretId); + } + + public System.Threading.Tasks.Task GetSecretLegacyAsync(string token, int secretId) { + return base.Channel.GetSecretLegacyAsync(token, secretId); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.GetSecretResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.GetSecret(Alkami.Ops.SecretServer.SSWebService.GetSecretRequest request) { + return base.Channel.GetSecret(request); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetSecret(string token, int secretId, System.Nullable loadSettingsAndPermissions, Alkami.Ops.SecretServer.SSWebService.CodeResponse[] codeResponses) { + Alkami.Ops.SecretServer.SSWebService.GetSecretRequest inValue = new Alkami.Ops.SecretServer.SSWebService.GetSecretRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.loadSettingsAndPermissions = loadSettingsAndPermissions; + inValue.codeResponses = codeResponses; + Alkami.Ops.SecretServer.SSWebService.GetSecretResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).GetSecret(inValue); + return retVal.GetSecretResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.GetSecretAsync(Alkami.Ops.SecretServer.SSWebService.GetSecretRequest request) { + return base.Channel.GetSecretAsync(request); + } + + public System.Threading.Tasks.Task GetSecretAsync(string token, int secretId, System.Nullable loadSettingsAndPermissions, Alkami.Ops.SecretServer.SSWebService.CodeResponse[] codeResponses) { + Alkami.Ops.SecretServer.SSWebService.GetSecretRequest inValue = new Alkami.Ops.SecretServer.SSWebService.GetSecretRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.loadSettingsAndPermissions = loadSettingsAndPermissions; + inValue.codeResponses = codeResponses; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).GetSecretAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.GetCheckOutStatusResult GetCheckOutStatus(string token, int secretId) { + return base.Channel.GetCheckOutStatus(token, secretId); + } + + public System.Threading.Tasks.Task GetCheckOutStatusAsync(string token, int secretId) { + return base.Channel.GetCheckOutStatusAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult ChangePassword(string token, string currentPassword, string newPassword) { + return base.Channel.ChangePassword(token, currentPassword, newPassword); + } + + public System.Threading.Tasks.Task ChangePasswordAsync(string token, string currentPassword, string newPassword) { + return base.Channel.ChangePasswordAsync(token, currentPassword, newPassword); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretsByFieldValueResult GetSecretsByFieldValue(string token, string fieldName, string searchTerm, bool showDeleted) { + return base.Channel.GetSecretsByFieldValue(token, fieldName, searchTerm, showDeleted); + } + + public System.Threading.Tasks.Task GetSecretsByFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted) { + return base.Channel.GetSecretsByFieldValueAsync(token, fieldName, searchTerm, showDeleted); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted) { + return base.Channel.SearchSecretsByFieldValue(token, fieldName, searchTerm, showDeleted, showRestricted); + } + + public System.Threading.Tasks.Task SearchSecretsByFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted) { + return base.Channel.SearchSecretsByFieldValueAsync(token, fieldName, searchTerm, showDeleted, showRestricted); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretsByFieldValueResult GetSecretsByExposedFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showPartialMatches) { + return base.Channel.GetSecretsByExposedFieldValue(token, fieldName, searchTerm, showDeleted, showPartialMatches); + } + + public System.Threading.Tasks.Task GetSecretsByExposedFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showPartialMatches) { + return base.Channel.GetSecretsByExposedFieldValueAsync(token, fieldName, searchTerm, showDeleted, showPartialMatches); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByExposedFieldValue(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches) { + return base.Channel.SearchSecretsByExposedFieldValue(token, fieldName, searchTerm, showDeleted, showRestricted, showPartialMatches); + } + + public System.Threading.Tasks.Task SearchSecretsByExposedFieldValueAsync(string token, string fieldName, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches) { + return base.Channel.SearchSecretsByExposedFieldValueAsync(token, fieldName, searchTerm, showDeleted, showRestricted, showPartialMatches); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByExposedValues(string token, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches) { + return base.Channel.SearchSecretsByExposedValues(token, searchTerm, showDeleted, showRestricted, showPartialMatches); + } + + public System.Threading.Tasks.Task SearchSecretsByExposedValuesAsync(string token, string searchTerm, bool showDeleted, bool showRestricted, bool showPartialMatches) { + return base.Channel.SearchSecretsByExposedValuesAsync(token, searchTerm, showDeleted, showRestricted, showPartialMatches); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddUser(string token, Alkami.Ops.SecretServer.SSWebService.User newUser) { + return base.Channel.AddUser(token, newUser); + } + + public System.Threading.Tasks.Task AddUserAsync(string token, Alkami.Ops.SecretServer.SSWebService.User newUser) { + return base.Channel.AddUserAsync(token, newUser); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecrets(Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest request) { + return base.Channel.SearchSecrets(request); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecrets(string token, string searchTerm, System.Nullable includeDeleted, System.Nullable includeRestricted) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.includeDeleted = includeDeleted; + inValue.includeRestricted = includeRestricted; + Alkami.Ops.SecretServer.SSWebService.SearchSecretsResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecrets(inValue); + return retVal.SearchSecretsResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecretsAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest request) { + return base.Channel.SearchSecretsAsync(request); + } + + public System.Threading.Tasks.Task SearchSecretsAsync(string token, string searchTerm, System.Nullable includeDeleted, System.Nullable includeRestricted) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.includeDeleted = includeDeleted; + inValue.includeRestricted = includeRestricted; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecretsAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsLegacy(string token, string searchTerm) { + return base.Channel.SearchSecretsLegacy(token, searchTerm); + } + + public System.Threading.Tasks.Task SearchSecretsLegacyAsync(string token, string searchTerm) { + return base.Channel.SearchSecretsLegacyAsync(token, searchTerm); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecretsByFolder(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest request) { + return base.Channel.SearchSecretsByFolder(request); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolder(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders, System.Nullable includeDeleted, System.Nullable includeRestricted) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.folderId = folderId; + inValue.includeSubFolders = includeSubFolders; + inValue.includeDeleted = includeDeleted; + inValue.includeRestricted = includeRestricted; + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecretsByFolder(inValue); + return retVal.SearchSecretsByFolderResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecretsByFolderAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest request) { + return base.Channel.SearchSecretsByFolderAsync(request); + } + + public System.Threading.Tasks.Task SearchSecretsByFolderAsync(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders, System.Nullable includeDeleted, System.Nullable includeRestricted) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.folderId = folderId; + inValue.includeSubFolders = includeSubFolders; + inValue.includeDeleted = includeDeleted; + inValue.includeRestricted = includeRestricted; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecretsByFolderAsync(inValue); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecretsByFolderLegacy(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest request) { + return base.Channel.SearchSecretsByFolderLegacy(request); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretsResult SearchSecretsByFolderLegacy(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.folderId = folderId; + inValue.includeSubFolders = includeSubFolders; + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecretsByFolderLegacy(inValue); + return retVal.SearchSecretsByFolderLegacyResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SearchSecretsByFolderLegacyAsync(Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest request) { + return base.Channel.SearchSecretsByFolderLegacyAsync(request); + } + + public System.Threading.Tasks.Task SearchSecretsByFolderLegacyAsync(string token, string searchTerm, System.Nullable folderId, bool includeSubFolders) { + Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SearchSecretsByFolderLegacyRequest(); + inValue.token = token; + inValue.searchTerm = searchTerm; + inValue.folderId = folderId; + inValue.includeSubFolders = includeSubFolders; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SearchSecretsByFolderLegacyAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.GetFavoritesResult GetFavorites(string token, bool includeRestricted) { + return base.Channel.GetFavorites(token, includeRestricted); + } + + public System.Threading.Tasks.Task GetFavoritesAsync(string token, bool includeRestricted) { + return base.Channel.GetFavoritesAsync(token, includeRestricted); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateIsFavorite(string token, int secretId, bool isFavorite) { + return base.Channel.UpdateIsFavorite(token, secretId, isFavorite); + } + + public System.Threading.Tasks.Task UpdateIsFavoriteAsync(string token, int secretId, bool isFavorite) { + return base.Channel.UpdateIsFavoriteAsync(token, secretId, isFavorite); + } + + public Alkami.Ops.SecretServer.SSWebService.AddSecretResult AddSecret(string token, int secretTypeId, string secretName, int[] secretFieldIds, string[] secretItemValues, int folderId) { + return base.Channel.AddSecret(token, secretTypeId, secretName, secretFieldIds, secretItemValues, folderId); + } + + public System.Threading.Tasks.Task AddSecretAsync(string token, int secretTypeId, string secretName, int[] secretFieldIds, string[] secretItemValues, int folderId) { + return base.Channel.AddSecretAsync(token, secretTypeId, secretName, secretFieldIds, secretItemValues, folderId); + } + + public Alkami.Ops.SecretServer.SSWebService.AddSecretResult AddNewSecret(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret) { + return base.Channel.AddNewSecret(token, secret); + } + + public System.Threading.Tasks.Task AddNewSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret) { + return base.Channel.AddNewSecretAsync(token, secret); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretResult GetNewSecret(string token, int secretTypeId, int folderId) { + return base.Channel.GetNewSecret(token, secretTypeId, folderId); + } + + public System.Threading.Tasks.Task GetNewSecretAsync(string token, int secretTypeId, int folderId) { + return base.Channel.GetNewSecretAsync(token, secretTypeId, folderId); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretTemplateFieldsResult GetSecretTemplateFields(string token, int secretTypeId) { + return base.Channel.GetSecretTemplateFields(token, secretTypeId); + } + + public System.Threading.Tasks.Task GetSecretTemplateFieldsAsync(string token, int secretTypeId) { + return base.Channel.GetSecretTemplateFieldsAsync(token, secretTypeId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateSecret(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret) { + return base.Channel.UpdateSecret(token, secret); + } + + public System.Threading.Tasks.Task UpdateSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.Secret secret) { + return base.Channel.UpdateSecretAsync(token, secret); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretTemplatesResult GetSecretTemplates(string token) { + return base.Channel.GetSecretTemplates(token); + } + + public System.Threading.Tasks.Task GetSecretTemplatesAsync(string token) { + return base.Channel.GetSecretTemplatesAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.GeneratePasswordResult GeneratePassword(string token, int secretFieldId) { + return base.Channel.GeneratePassword(token, secretFieldId); + } + + public System.Threading.Tasks.Task GeneratePasswordAsync(string token, int secretFieldId) { + return base.Channel.GeneratePasswordAsync(token, secretFieldId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult DeactivateSecret(string token, int secretId) { + return base.Channel.DeactivateSecret(token, secretId); + } + + public System.Threading.Tasks.Task DeactivateSecretAsync(string token, int secretId) { + return base.Channel.DeactivateSecretAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.VersionGetResult VersionGet() { + return base.Channel.VersionGet(); + } + + public System.Threading.Tasks.Task VersionGetAsync() { + return base.Channel.VersionGetAsync(); + } + + public Alkami.Ops.SecretServer.SSWebService.GetFolderResult FolderGet(string token, int folderId) { + return base.Channel.FolderGet(token, folderId); + } + + public System.Threading.Tasks.Task FolderGetAsync(string token, int folderId) { + return base.Channel.FolderGetAsync(token, folderId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult FolderUpdate(string token, Alkami.Ops.SecretServer.SSWebService.Folder modifiedFolder) { + return base.Channel.FolderUpdate(token, modifiedFolder); + } + + public System.Threading.Tasks.Task FolderUpdateAsync(string token, Alkami.Ops.SecretServer.SSWebService.Folder modifiedFolder) { + return base.Channel.FolderUpdateAsync(token, modifiedFolder); + } + + public Alkami.Ops.SecretServer.SSWebService.GetFoldersResult FolderGetAllChildren(string token, int parentFolderId) { + return base.Channel.FolderGetAllChildren(token, parentFolderId); + } + + public System.Threading.Tasks.Task FolderGetAllChildrenAsync(string token, int parentFolderId) { + return base.Channel.FolderGetAllChildrenAsync(token, parentFolderId); + } + + public Alkami.Ops.SecretServer.SSWebService.CreateFolderResult FolderCreate(string token, string folderName, int parentFolderId, int folderTypeId) { + return base.Channel.FolderCreate(token, folderName, parentFolderId, folderTypeId); + } + + public System.Threading.Tasks.Task FolderCreateAsync(string token, string folderName, int parentFolderId, int folderTypeId) { + return base.Channel.FolderCreateAsync(token, folderName, parentFolderId, folderTypeId); + } + + public Alkami.Ops.SecretServer.SSWebService.FolderExtendedCreateResult FolderExtendedCreate(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder) { + return base.Channel.FolderExtendedCreate(token, folder); + } + + public System.Threading.Tasks.Task FolderExtendedCreateAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder) { + return base.Channel.FolderExtendedCreateAsync(token, folder); + } + + public Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetResult FolderExtendedGet(string token, int folderId) { + return base.Channel.FolderExtendedGet(token, folderId); + } + + public System.Threading.Tasks.Task FolderExtendedGetAsync(string token, int folderId) { + return base.Channel.FolderExtendedGetAsync(token, folderId); + } + + public Alkami.Ops.SecretServer.SSWebService.FolderExtendedUpdateResult FolderExtendedUpdate(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder) { + return base.Channel.FolderExtendedUpdate(token, folder); + } + + public System.Threading.Tasks.Task FolderExtendedUpdateAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtended folder) { + return base.Channel.FolderExtendedUpdateAsync(token, folder); + } + + public Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewResult FolderExtendedGetNew(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewRequest folderExtendedGetNewRequest) { + return base.Channel.FolderExtendedGetNew(token, folderExtendedGetNewRequest); + } + + public System.Threading.Tasks.Task FolderExtendedGetNewAsync(string token, Alkami.Ops.SecretServer.SSWebService.FolderExtendedGetNewRequest folderExtendedGetNewRequest) { + return base.Channel.FolderExtendedGetNewAsync(token, folderExtendedGetNewRequest); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchFolderResult SearchFolders(string token, string folderName) { + return base.Channel.SearchFolders(token, folderName); + } + + public System.Threading.Tasks.Task SearchFoldersAsync(string token, string folderName) { + return base.Channel.SearchFoldersAsync(token, folderName); + } + + public Alkami.Ops.SecretServer.SSWebService.FileDownloadResult DownloadFileAttachment(string token, int secretId) { + return base.Channel.DownloadFileAttachment(token, secretId); + } + + public System.Threading.Tasks.Task DownloadFileAttachmentAsync(string token, int secretId) { + return base.Channel.DownloadFileAttachmentAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.FileDownloadResult DownloadFileAttachmentByItemId(string token, int secretId, int secretItemId) { + return base.Channel.DownloadFileAttachmentByItemId(token, secretId, secretItemId); + } + + public System.Threading.Tasks.Task DownloadFileAttachmentByItemIdAsync(string token, int secretId, int secretItemId) { + return base.Channel.DownloadFileAttachmentByItemIdAsync(token, secretId, secretItemId); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.UploadFileAttachment(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest request) { + return base.Channel.UploadFileAttachment(request); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachment(string token, int secretId, byte[] fileData, string fileName) { + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest inValue = new Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.fileData = fileData; + inValue.fileName = fileName; + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).UploadFileAttachment(inValue); + return retVal.UploadFileAttachmentResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.UploadFileAttachmentAsync(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest request) { + return base.Channel.UploadFileAttachmentAsync(request); + } + + public System.Threading.Tasks.Task UploadFileAttachmentAsync(string token, int secretId, byte[] fileData, string fileName) { + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest inValue = new Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.fileData = fileData; + inValue.fileName = fileName; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).UploadFileAttachmentAsync(inValue); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.UploadFileAttachmentByItemId(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest request) { + return base.Channel.UploadFileAttachmentByItemId(request); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UploadFileAttachmentByItemId(string token, int secretId, int secretItemId, byte[] fileData, string fileName) { + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest inValue = new Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.secretItemId = secretItemId; + inValue.fileData = fileData; + inValue.fileName = fileName; + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).UploadFileAttachmentByItemId(inValue); + return retVal.UploadFileAttachmentByItemIdResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.UploadFileAttachmentByItemIdAsync(Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest request) { + return base.Channel.UploadFileAttachmentByItemIdAsync(request); + } + + public System.Threading.Tasks.Task UploadFileAttachmentByItemIdAsync(string token, int secretId, int secretItemId, byte[] fileData, string fileName) { + Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest inValue = new Alkami.Ops.SecretServer.SSWebService.UploadFileAttachmentByItemIdRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.secretItemId = secretItemId; + inValue.fileData = fileData; + inValue.fileName = fileName; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).UploadFileAttachmentByItemIdAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult ExpireSecret(string token, int secretId) { + return base.Channel.ExpireSecret(token, secretId); + } + + public System.Threading.Tasks.Task ExpireSecretAsync(string token, int secretId) { + return base.Channel.ExpireSecretAsync(token, secretId); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SetCheckOutEnabled(Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest request) { + return base.Channel.SetCheckOutEnabled(request); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult SetCheckOutEnabled(string token, int secretId, bool setCheckOut, bool setPasswordChangeOnCheckIn, System.Nullable checkOutInterval) { + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.setCheckOut = setCheckOut; + inValue.setPasswordChangeOnCheckIn = setPasswordChangeOnCheckIn; + inValue.checkOutInterval = checkOutInterval; + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SetCheckOutEnabled(inValue); + return retVal.SetCheckOutEnabledResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.SetCheckOutEnabledAsync(Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest request) { + return base.Channel.SetCheckOutEnabledAsync(request); + } + + public System.Threading.Tasks.Task SetCheckOutEnabledAsync(string token, int secretId, bool setCheckOut, bool setPasswordChangeOnCheckIn, System.Nullable checkOutInterval) { + Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest inValue = new Alkami.Ops.SecretServer.SSWebService.SetCheckOutEnabledRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.setCheckOut = setCheckOut; + inValue.setPasswordChangeOnCheckIn = setPasswordChangeOnCheckIn; + inValue.checkOutInterval = checkOutInterval; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).SetCheckOutEnabledAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult ImportXML(string token, string xml) { + return base.Channel.ImportXML(token, xml); + } + + public System.Threading.Tasks.Task ImportXMLAsync(string token, string xml) { + return base.Channel.ImportXMLAsync(token, xml); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSecretAuditResult GetSecretAudit(string token, int secretId) { + return base.Channel.GetSecretAudit(token, secretId); + } + + public System.Threading.Tasks.Task GetSecretAuditAsync(string token, int secretId) { + return base.Channel.GetSecretAuditAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddDependency(string token, Alkami.Ops.SecretServer.SSWebService.Dependency dependency) { + return base.Channel.AddDependency(token, dependency); + } + + public System.Threading.Tasks.Task AddDependencyAsync(string token, Alkami.Ops.SecretServer.SSWebService.Dependency dependency) { + return base.Channel.AddDependencyAsync(token, dependency); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult RemoveDependency(string token, int dependencyId, int secretId) { + return base.Channel.RemoveDependency(token, dependencyId, secretId); + } + + public System.Threading.Tasks.Task RemoveDependencyAsync(string token, int dependencyId, int secretId) { + return base.Channel.RemoveDependencyAsync(token, dependencyId, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.GetDependenciesResult GetDependencies(string token, int secretId) { + return base.Channel.GetDependencies(token, secretId); + } + + public System.Threading.Tasks.Task GetDependenciesAsync(string token, int secretId) { + return base.Channel.GetDependenciesAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult CreateDependencyGroupForSecret(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup) { + return base.Channel.CreateDependencyGroupForSecret(token, dependencyGroup); + } + + public System.Threading.Tasks.Task CreateDependencyGroupForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup) { + return base.Channel.CreateDependencyGroupForSecretAsync(token, dependencyGroup); + } + + public Alkami.Ops.SecretServer.SSWebService.GetDependencyGroupsResult GetDependencyGroupsForSecret(string token, int secretId) { + return base.Channel.GetDependencyGroupsForSecret(token, secretId); + } + + public System.Threading.Tasks.Task GetDependencyGroupsForSecretAsync(string token, int secretId) { + return base.Channel.GetDependencyGroupsForSecretAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateDependencyGroupForSecret(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup) { + return base.Channel.UpdateDependencyGroupForSecret(token, dependencyGroup); + } + + public System.Threading.Tasks.Task UpdateDependencyGroupForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.DependencyGroup dependencyGroup) { + return base.Channel.UpdateDependencyGroupForSecretAsync(token, dependencyGroup); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult RemoveDependencyGroupForSecret(string token, int dependencyGroupId) { + return base.Channel.RemoveDependencyGroupForSecret(token, dependencyGroupId); + } + + public System.Threading.Tasks.Task RemoveDependencyGroupForSecretAsync(string token, int dependencyGroupId) { + return base.Channel.RemoveDependencyGroupForSecretAsync(token, dependencyGroupId); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSitesResult GetDistributedEngines(string token) { + return base.Channel.GetDistributedEngines(token); + } + + public System.Threading.Tasks.Task GetDistributedEnginesAsync(string token) { + return base.Channel.GetDistributedEnginesAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.GetTicketSystemsResult GetTicketSystems(string token) { + return base.Channel.GetTicketSystems(token); + } + + public System.Threading.Tasks.Task GetTicketSystemsAsync(string token) { + return base.Channel.GetTicketSystemsAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AssignSite(string token, int secretId, int siteId) { + return base.Channel.AssignSite(token, secretId, siteId); + } + + public System.Threading.Tasks.Task AssignSiteAsync(string token, int secretId, int siteId) { + return base.Channel.AssignSiteAsync(token, secretId, siteId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult CheckIn(string token, int secretId) { + return base.Channel.CheckIn(token, secretId); + } + + public System.Threading.Tasks.Task CheckInAsync(string token, int secretId) { + return base.Channel.CheckInAsync(token, secretId); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.AddSecretCustomAudit(Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest request) { + return base.Channel.AddSecretCustomAudit(request); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddSecretCustomAudit(string token, int secretId, string notes, string ipAddress, System.Nullable referenceId, string ticketNumber, int userId) { + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest inValue = new Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.notes = notes; + inValue.ipAddress = ipAddress; + inValue.referenceId = referenceId; + inValue.ticketNumber = ticketNumber; + inValue.userId = userId; + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).AddSecretCustomAudit(inValue); + return retVal.AddSecretCustomAuditResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.AddSecretCustomAuditAsync(Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest request) { + return base.Channel.AddSecretCustomAuditAsync(request); + } + + public System.Threading.Tasks.Task AddSecretCustomAuditAsync(string token, int secretId, string notes, string ipAddress, System.Nullable referenceId, string ticketNumber, int userId) { + Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest inValue = new Alkami.Ops.SecretServer.SSWebService.AddSecretCustomAuditRequest(); + inValue.token = token; + inValue.secretId = secretId; + inValue.notes = notes; + inValue.ipAddress = ipAddress; + inValue.referenceId = referenceId; + inValue.ticketNumber = ticketNumber; + inValue.userId = userId; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).AddSecretCustomAuditAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult UpdateSecretPermission(string token, int secretId, Alkami.Ops.SecretServer.SSWebService.GroupOrUserRecord groupOrUserRecord, bool view, bool edit, bool owner) { + return base.Channel.UpdateSecretPermission(token, secretId, groupOrUserRecord, view, edit, owner); + } + + public System.Threading.Tasks.Task UpdateSecretPermissionAsync(string token, int secretId, Alkami.Ops.SecretServer.SSWebService.GroupOrUserRecord groupOrUserRecord, bool view, bool edit, bool owner) { + return base.Channel.UpdateSecretPermissionAsync(token, secretId, groupOrUserRecord, view, edit, owner); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult CheckInByKey(string sessionKey) { + return base.Channel.CheckInByKey(sessionKey); + } + + public System.Threading.Tasks.Task CheckInByKeyAsync(string sessionKey) { + return base.Channel.CheckInByKeyAsync(sessionKey); + } + + public Alkami.Ops.SecretServer.SSWebService.UserInfoResult WhoAmI(string token) { + return base.Channel.WhoAmI(token); + } + + public System.Threading.Tasks.Task WhoAmIAsync(string token) { + return base.Channel.WhoAmIAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.GetAllGroupsResult GetAllGroups(string token) { + return base.Channel.GetAllGroups(token); + } + + public System.Threading.Tasks.Task GetAllGroupsAsync(string token) { + return base.Channel.GetAllGroupsAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AssignUserToGroup(string token, int userId, int groupId) { + return base.Channel.AssignUserToGroup(token, userId, groupId); + } + + public System.Threading.Tasks.Task AssignUserToGroupAsync(string token, int userId, int groupId) { + return base.Channel.AssignUserToGroupAsync(token, userId, groupId); + } + + public Alkami.Ops.SecretServer.SSWebService.SSHCredentialsResult GetSSHLoginCredentials(string token, int secretId) { + return base.Channel.GetSSHLoginCredentials(token, secretId); + } + + public System.Threading.Tasks.Task GetSSHLoginCredentialsAsync(string token, int secretId) { + return base.Channel.GetSSHLoginCredentialsAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.SSHCredentialsResult GetSSHLoginCredentialsWithMachine(string token, int secretId, string machine) { + return base.Channel.GetSSHLoginCredentialsWithMachine(token, secretId, machine); + } + + public System.Threading.Tasks.Task GetSSHLoginCredentialsWithMachineAsync(string token, int secretId, string machine) { + return base.Channel.GetSSHLoginCredentialsWithMachineAsync(token, secretId, machine); + } + + public Alkami.Ops.SecretServer.SSWebService.GetUsersResult SearchUsers(string token, string searchTerm, bool includeInactiveUsers) { + return base.Channel.SearchUsers(token, searchTerm, includeInactiveUsers); + } + + public System.Threading.Tasks.Task SearchUsersAsync(string token, string searchTerm, bool includeInactiveUsers) { + return base.Channel.SearchUsersAsync(token, searchTerm, includeInactiveUsers); + } + + public Alkami.Ops.SecretServer.SSWebService.GetUserResult GetUser(string token, int userId) { + return base.Channel.GetUser(token, userId); + } + + public System.Threading.Tasks.Task GetUserAsync(string token, int userId) { + return base.Channel.GetUserAsync(token, userId); + } + + public Alkami.Ops.SecretServer.SSWebService.UpdateUserResult UpdateUser(string token, Alkami.Ops.SecretServer.SSWebService.User user) { + return base.Channel.UpdateUser(token, user); + } + + public System.Threading.Tasks.Task UpdateUserAsync(string token, Alkami.Ops.SecretServer.SSWebService.User user) { + return base.Channel.UpdateUserAsync(token, user); + } + + public Alkami.Ops.SecretServer.SSWebService.SecretItemHistoryResult GetSecretItemHistoryByFieldName(string token, int secretId, string fieldDisplayName) { + return base.Channel.GetSecretItemHistoryByFieldName(token, secretId, fieldDisplayName); + } + + public System.Threading.Tasks.Task GetSecretItemHistoryByFieldNameAsync(string token, int secretId, string fieldDisplayName) { + return base.Channel.GetSecretItemHistoryByFieldNameAsync(token, secretId, fieldDisplayName); + } + + public Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecretResult GetSecretPolicyForSecret(string token, int secretId) { + return base.Channel.GetSecretPolicyForSecret(token, secretId); + } + + public System.Threading.Tasks.Task GetSecretPolicyForSecretAsync(string token, int secretId) { + return base.Channel.GetSecretPolicyForSecretAsync(token, secretId); + } + + public Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecretResult AssignSecretPolicyForSecret(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecret secretPolicyForSecret) { + return base.Channel.AssignSecretPolicyForSecret(token, secretPolicyForSecret); + } + + public System.Threading.Tasks.Task AssignSecretPolicyForSecretAsync(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyForSecret secretPolicyForSecret) { + return base.Channel.AssignSecretPolicyForSecretAsync(token, secretPolicyForSecret); + } + + public Alkami.Ops.SecretServer.SSWebService.SearchSecretPoliciesResult SearchSecretPolicies(string token, string term, bool includeInactive) { + return base.Channel.SearchSecretPolicies(token, term, includeInactive); + } + + public System.Threading.Tasks.Task SearchSecretPoliciesAsync(string token, string term, bool includeInactive) { + return base.Channel.SearchSecretPoliciesAsync(token, term, includeInactive); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult RunActiveDirectorySynchronization(string token) { + return base.Channel.RunActiveDirectorySynchronization(token); + } + + public System.Threading.Tasks.Task RunActiveDirectorySynchronizationAsync(string token) { + return base.Channel.RunActiveDirectorySynchronizationAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddGroupToActiveDirectorySynchronization(string token, Alkami.Ops.SecretServer.SSWebService.AddGroupRequestMessage addGroupRequestMessage) { + return base.Channel.AddGroupToActiveDirectorySynchronization(token, addGroupRequestMessage); + } + + public System.Threading.Tasks.Task AddGroupToActiveDirectorySynchronizationAsync(string token, Alkami.Ops.SecretServer.SSWebService.AddGroupRequestMessage addGroupRequestMessage) { + return base.Channel.AddGroupToActiveDirectorySynchronizationAsync(token, addGroupRequestMessage); + } + + public Alkami.Ops.SecretServer.SSWebService.SecretPolicyResult AddSecretPolicy(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyDetail secretPolicy) { + return base.Channel.AddSecretPolicy(token, secretPolicy); + } + + public System.Threading.Tasks.Task AddSecretPolicyAsync(string token, Alkami.Ops.SecretServer.SSWebService.SecretPolicyDetail secretPolicy) { + return base.Channel.AddSecretPolicyAsync(token, secretPolicy); + } + + public Alkami.Ops.SecretServer.SSWebService.SecretPolicyResult GetNewSecretPolicy(string token) { + return base.Channel.GetNewSecretPolicy(token); + } + + public System.Threading.Tasks.Task GetNewSecretPolicyAsync(string token) { + return base.Channel.GetNewSecretPolicyAsync(token); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenuResult GetSSHCommandMenu(string token, int sshCommandMenuId) { + return base.Channel.GetSSHCommandMenu(token, sshCommandMenuId); + } + + public System.Threading.Tasks.Task GetSSHCommandMenuAsync(string token, int sshCommandMenuId) { + return base.Channel.GetSSHCommandMenuAsync(token, sshCommandMenuId); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenuResult SaveSSHCommandMenu(string token, Alkami.Ops.SecretServer.SSWebService.SshCommandMenu sshCommandMenu, string commandsText, bool deleteCommands) { + return base.Channel.SaveSSHCommandMenu(token, sshCommandMenu, commandsText, deleteCommands); + } + + public System.Threading.Tasks.Task SaveSSHCommandMenuAsync(string token, Alkami.Ops.SecretServer.SSWebService.SshCommandMenu sshCommandMenu, string commandsText, bool deleteCommands) { + return base.Channel.SaveSSHCommandMenuAsync(token, sshCommandMenu, commandsText, deleteCommands); + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusResponse Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.GetAllSSHCommandMenus(Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest request) { + return base.Channel.GetAllSSHCommandMenus(request); + } + + public Alkami.Ops.SecretServer.SSWebService.GetSshCommandMenusResult GetAllSSHCommandMenus(string token, System.Nullable includeInactive) { + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest inValue = new Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest(); + inValue.token = token; + inValue.includeInactive = includeInactive; + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusResponse retVal = ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).GetAllSSHCommandMenus(inValue); + return retVal.GetAllSSHCommandMenusResult; + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + System.Threading.Tasks.Task Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap.GetAllSSHCommandMenusAsync(Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest request) { + return base.Channel.GetAllSSHCommandMenusAsync(request); + } + + public System.Threading.Tasks.Task GetAllSSHCommandMenusAsync(string token, System.Nullable includeInactive) { + Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest inValue = new Alkami.Ops.SecretServer.SSWebService.GetAllSSHCommandMenusRequest(); + inValue.token = token; + inValue.includeInactive = includeInactive; + return ((Alkami.Ops.SecretServer.SSWebService.SSWebServiceSoap)(this)).GetAllSSHCommandMenusAsync(inValue); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult DeleteSSHCommandMenu(string token, int sshCommandMenuId) { + return base.Channel.DeleteSSHCommandMenu(token, sshCommandMenuId); + } + + public System.Threading.Tasks.Task DeleteSSHCommandMenuAsync(string token, int sshCommandMenuId) { + return base.Channel.DeleteSSHCommandMenuAsync(token, sshCommandMenuId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult RestoreSSHCommandMenu(string token, int sshCommandMenuId) { + return base.Channel.RestoreSSHCommandMenu(token, sshCommandMenuId); + } + + public System.Threading.Tasks.Task RestoreSSHCommandMenuAsync(string token, int sshCommandMenuId) { + return base.Channel.RestoreSSHCommandMenuAsync(token, sshCommandMenuId); + } + + public Alkami.Ops.SecretServer.SSWebService.WebServiceResult AddScript(string token, Alkami.Ops.SecretServer.SSWebService.UserScript newUserScript) { + return base.Channel.AddScript(token, newUserScript); + } + + public System.Threading.Tasks.Task AddScriptAsync(string token, Alkami.Ops.SecretServer.SSWebService.UserScript newUserScript) { + return base.Channel.AddScriptAsync(token, newUserScript); + } + + public Alkami.Ops.SecretServer.SSWebService.GetUserScriptsResult GetAllScripts(string token, bool includeInactiveUserScripts) { + return base.Channel.GetAllScripts(token, includeInactiveUserScripts); + } + + public System.Threading.Tasks.Task GetAllScriptsAsync(string token, bool includeInactiveUserScripts) { + return base.Channel.GetAllScriptsAsync(token, includeInactiveUserScripts); + } + + public Alkami.Ops.SecretServer.SSWebService.GetUserScriptResult GetScript(string token, int userScriptId) { + return base.Channel.GetScript(token, userScriptId); + } + + public System.Threading.Tasks.Task GetScriptAsync(string token, int userScriptId) { + return base.Channel.GetScriptAsync(token, userScriptId); + } + + public Alkami.Ops.SecretServer.SSWebService.UpdateUserScriptResult UpdateScript(string token, Alkami.Ops.SecretServer.SSWebService.UserScript userScript) { + return base.Channel.UpdateScript(token, userScript); + } + + public System.Threading.Tasks.Task UpdateScriptAsync(string token, Alkami.Ops.SecretServer.SSWebService.UserScript userScript) { + return base.Channel.UpdateScriptAsync(token, userScript); + } + } +} diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.svcmap b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.svcmap new file mode 100644 index 0000000..b1a1ca7 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/Reference.svcmap @@ -0,0 +1,32 @@ + + + + false + true + true + + false + false + false + + + true + Auto + true + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.disco b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.disco new file mode 100644 index 0000000..b84e549 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.disco @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.wsdl b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.wsdl new file mode 100644 index 0000000..9bd56ad --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/SSWebService.wsdl @@ -0,0 +1,7487 @@ + + + Webservice for standard integration. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webservice for standard integration. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration.svcinfo b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration.svcinfo new file mode 100644 index 0000000..0569c28 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration.svcinfo @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration91.svcinfo b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration91.svcinfo new file mode 100644 index 0000000..fcc7b7a --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/Service References/SSWebService/configuration91.svcinfo @@ -0,0 +1,549 @@ + + + + + + + SSWebServiceSoap + + + + + + + + + + + + + + + + + + + + + StrongWildcard + + + + + + 65536 + + + + + + + + + System.ServiceModel.Configuration.XmlDictionaryReaderQuotasElement + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + System.Text.UTF8Encoding + + + Buffered + + + + + + Text + + + System.ServiceModel.Configuration.BasicHttpSecurityElement + + + Transport + + + System.ServiceModel.Configuration.HttpTransportSecurityElement + + + None + + + None + + + System.Security.Authentication.ExtendedProtection.Configuration.ExtendedProtectionPolicyElement + + + Never + + + TransportSelected + + + (Collection) + + + + + + System.ServiceModel.Configuration.BasicHttpMessageSecurityElement + + + UserName + + + Default + + + + + + + SSWebServiceSoap1 + + + + + + + + + + + + + + + + + + + + + StrongWildcard + + + + + + 65536 + + + + + + + + + System.ServiceModel.Configuration.XmlDictionaryReaderQuotasElement + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + System.Text.UTF8Encoding + + + Buffered + + + + + + Text + + + System.ServiceModel.Configuration.BasicHttpSecurityElement + + + None + + + System.ServiceModel.Configuration.HttpTransportSecurityElement + + + None + + + None + + + System.Security.Authentication.ExtendedProtection.Configuration.ExtendedProtectionPolicyElement + + + Never + + + TransportSelected + + + (Collection) + + + + + + System.ServiceModel.Configuration.BasicHttpMessageSecurityElement + + + UserName + + + Default + + + + + + + SSWebServiceSoap12 + + + + + + + + + + + + + + + System.ServiceModel.Configuration.TextMessageEncodingElement + + + 64 + + + 16 + + + Soap12 + + + System.ServiceModel.Configuration.XmlDictionaryReaderQuotasElement + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + System.Text.UTF8Encoding + + + System.ServiceModel.Configuration.HttpsTransportElement + + + False + + + 524288 + + + 65536 + + + False + + + 00:00:00 + + + Anonymous + + + False + + + True + + + StrongWildcard + + + True + + + 65536 + + + 0 + + + System.ServiceModel.Configuration.HttpMessageHandlerFactoryElement + + + (Collection) + + + + + + + + + Anonymous + + + + + + Buffered + + + False + + + True + + + System.Security.Authentication.ExtendedProtection.Configuration.ExtendedProtectionPolicyElement + + + Never + + + TransportSelected + + + (Collection) + + + System.ServiceModel.Configuration.WebSocketTransportSettingsElement + + + Never + + + False + + + 00:00:00 + + + + + + False + + + 0 + + + False + + + + + + + + + https://secret.corp.alkamitech.com/webservices/SSWebService.asmx + + + + + + basicHttpBinding + + + SSWebServiceSoap + + + SSWebService.SSWebServiceSoap + + + System.ServiceModel.Configuration.AddressHeaderCollectionElement + + + <Header /> + + + System.ServiceModel.Configuration.IdentityElement + + + System.ServiceModel.Configuration.UserPrincipalNameElement + + + + + + System.ServiceModel.Configuration.ServicePrincipalNameElement + + + + + + System.ServiceModel.Configuration.DnsElement + + + + + + System.ServiceModel.Configuration.RsaElement + + + + + + System.ServiceModel.Configuration.CertificateElement + + + + + + System.ServiceModel.Configuration.CertificateReferenceElement + + + My + + + LocalMachine + + + FindBySubjectDistinguishedName + + + + + + False + + + SSWebServiceSoap + + + + + + + + + + + + + https://secret.corp.alkamitech.com/webservices/SSWebService.asmx + + + + + + customBinding + + + SSWebServiceSoap12 + + + SSWebService.SSWebServiceSoap + + + System.ServiceModel.Configuration.AddressHeaderCollectionElement + + + <Header /> + + + System.ServiceModel.Configuration.IdentityElement + + + System.ServiceModel.Configuration.UserPrincipalNameElement + + + + + + System.ServiceModel.Configuration.ServicePrincipalNameElement + + + + + + System.ServiceModel.Configuration.DnsElement + + + + + + System.ServiceModel.Configuration.RsaElement + + + + + + System.ServiceModel.Configuration.CertificateElement + + + + + + System.ServiceModel.Configuration.CertificateReferenceElement + + + My + + + LocalMachine + + + FindBySubjectDistinguishedName + + + + + + False + + + SSWebServiceSoap12 + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/app.config b/Modules/Alkami.Ops.SecretServer/app.config new file mode 100644 index 0000000..9cd5f61 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/app.config @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/tools/chocolateyInstall.ps1 b/Modules/Alkami.Ops.SecretServer/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.Ops.SecretServer/tools/chocolateyUninstall.ps1 b/Modules/Alkami.Ops.SecretServer/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.Ops.SecretServer/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.nuspec b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.nuspec new file mode 100644 index 0000000..7f3635c --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.nuspec @@ -0,0 +1,30 @@ + + + + Alkami.PowerShell.AD + $version$ + Alkami Platform Modules - PowerShell - AD + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.AD + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami AD module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.psd1 b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.psd1 new file mode 100644 index 0000000..db01a04 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.psd1 @@ -0,0 +1,12 @@ +@{ + RootModule = 'Alkami.PowerShell.AD.psm1' + ModuleVersion = '3.20.4' + GUID = '56a6af93-a660-4a63-9561-fd3be354ae10' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing Windows Active Directory' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Add-UsersToLocalSecurityGroup','Get-ComputerOU','Get-WindowsServiceUser','Install-ActiveDirectoryModule','Test-AppTierGMSAAccounts','Test-OrInstallADServiceAccount' +} diff --git a/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.pssproj b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.pssproj new file mode 100644 index 0000000..ee52c1d --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Alkami.PowerShell.AD.pssproj @@ -0,0 +1,53 @@ + + + Debug + 2.0 + {5a68c472-9179-4910-ab2f-5c3467f58e5e} + Exe + MyApplication + MyApplication + Alkami.PowerShell.AD + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.AD") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.AD/AlkamiManifest.xml b/Modules/Alkami.PowerShell.AD/AlkamiManifest.xml new file mode 100644 index 0000000..1f69c4a --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.AD + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.AD/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.AD/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Alkami.PowerShell.AD/Public/Add-UsersToLocalSecurityGroup.ps1 b/Modules/Alkami.PowerShell.AD/Public/Add-UsersToLocalSecurityGroup.ps1 new file mode 100644 index 0000000..bb60fcd --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Add-UsersToLocalSecurityGroup.ps1 @@ -0,0 +1,29 @@ +function Add-UsersToLocalSecurityGroup { +<# +.SYNOPSIS + Adds the Specified Users to a Local Security Group +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]]$users, + + [Parameter(Mandatory = $true)] + [string]$groupName + ) + + $logLead = (Get-LogLeadName); + Write-Verbose ("$logLead : Adding users {0} to security group {1}" -f ($users -join ", "), $groupName) + + foreach ($user in $users) { + if ($null -ne (Get-LocalGroupMember -Group $groupName -Member $user -ErrorAction SilentlyContinue)) { + Write-Output ("$logLead : User {0} is already a member of group {1}" -f $user, $groupName) + } + else { + Write-Output ("$logLead : Adding user {0} to local security group {1}" -f $user, $groupName) + Add-LocalGroupMember -Group $groupName -Member $user + } + } +} + diff --git a/Modules/Alkami.PowerShell.AD/Public/Get-ComputerOU.ps1 b/Modules/Alkami.PowerShell.AD/Public/Get-ComputerOU.ps1 new file mode 100644 index 0000000..ff75843 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Get-ComputerOU.ps1 @@ -0,0 +1,34 @@ +function Get-ComputerOU { +<# +.SYNOPSIS + Retrieves the fully distinguished OU of the current machine +#> + + [CmdletBinding()] + Param([string]$machineName) + $logLead = (Get-LogLeadName); + + if ([String]::IsNullOrEmpty($machineName)) { + Write-Verbose ("$logLead : `$machineName parameter not specified, using {0}" -f $env:COMPUTERNAME) + $machineName = $env:COMPUTERNAME + } + + $ldapString = "LDAP://" + [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName + $domainRoot = New-Object System.DirectoryServices.DirectoryEntry($ldapString) + + $ds = New-Object System.DirectoryServices.DirectorySearcher($domainRoot) + $ds.Filter = "(&(objectClass=computer)(name=$machineName))" + Write-Verbose ("$logLead : Searching domain with filter {0}" -f $ds.Filter) + [System.DirectoryServices.SearchResult]$searchResult = $ds.FindOne() + + if ($null -eq $searchResult) { + Write-Output ("$logLead : {0}" -f $errors[0] | Format-List -f) + Write-Warning "$logLead : Unable to find the current computer object" + return $null + } + + $distinguishedName = $searchResult.Properties["distinguishedName"] + Write-Verbose ("$logLead : DistinguishedName read as {0}" -f $distinguishedName.Substring($machineName.Length + 4)) + return $distinguishedName.Substring($machineName.Length + 4) +} + diff --git a/Modules/Alkami.PowerShell.AD/Public/Get-WindowsServiceUser.ps1 b/Modules/Alkami.PowerShell.AD/Public/Get-WindowsServiceUser.ps1 new file mode 100644 index 0000000..dab986d --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Get-WindowsServiceUser.ps1 @@ -0,0 +1,21 @@ +function Get-WindowsServiceUser { +<# +.SYNOPSIS + Returns the username which runs a particular service +#> + param( + [string]$ServiceName + ) + + $service = Get-Service -Name $ServiceName -ComputerName . -ErrorAction SilentlyContinue + + if ($null -eq $service) { + Write-Warning ("Service [{0}] could not be located." -f $ServiceName) + return $null + } + + $serviceAccount = (Get-CIMInstance Win32_Service -Filter ("Name = '{0}'" -f $service.Name)).StartName; + + return $serviceAccount +} + diff --git a/Modules/Alkami.PowerShell.AD/Public/Install-ActiveDirectoryModule.ps1 b/Modules/Alkami.PowerShell.AD/Public/Install-ActiveDirectoryModule.ps1 new file mode 100644 index 0000000..da2b5b4 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Install-ActiveDirectoryModule.ps1 @@ -0,0 +1,34 @@ +function Install-ActiveDirectoryModule { + <# +.SYNOPSIS + Installs the ActiveDirectory module by Enabling the WindowsFeature RSAT-AD-PowerShell + +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + ## TODO: Anyone ~ Make this work on client operating systems, which relies on a hotfix installation and Enable-WindowsOptionalFeature + ## dism /online /add-capability /capabilityname:Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 + + $logLead = Get-LogLeadName + + # Try to load the module + Import-Module ActiveDirectory -ErrorAction SilentlyContinue -Verbose:$false + + if (($null -ne $error[0]) -and ($null -ne $error[0].Exception) -and ($error[0].Exception.Message.StartsWith("The specified module 'ActiveDirectory'"))) { + Write-Output "$logLead : The active directory module is not installed. Attempting to install it..." + $result = Add-WindowsFeature RSAT-AD-PowerShell -Verbose:$false + + if ($result.ExitCode -ne "Success") { + Write-Warning "$logLead : Installation of the RSAT module failed." + return $false + } else { + Write-Output "$logLead : RSAT Module Installed Successfully. Loading it to host" + Import-Module ActiveDirectory + } + } + + return $true +} + diff --git a/Modules/Alkami.PowerShell.AD/Public/Test-AppTierGMSAAccounts.ps1 b/Modules/Alkami.PowerShell.AD/Public/Test-AppTierGMSAAccounts.ps1 new file mode 100644 index 0000000..e56d1e4 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Test-AppTierGMSAAccounts.ps1 @@ -0,0 +1,30 @@ +function Test-AppTierGMSAAccounts { + <# +.SYNOPSIS + Tests GMSA Accounts for App Tier applications and Services + +.LINK + Install-ActiveDirectoryModule + +.LINK + Test-OrInstallADServiceAccount +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + $logLead = (Get-LogLeadName); + + if (!(Install-ActiveDirectoryModule)) { + Write-Warning ("$logLead : Unable to install or load the ActiveDirectory module. GMSA accounts cannot be installed or tested and must be verified post-installation") + return + } + + foreach ($application in ($appTierApplications | Where-Object {$_.User.EndsWith("$") -and $_.IsGMSAAccount})) { + Test-OrInstallADServiceAccount $application.User + } + + foreach ($service in ((Get-AppTierServices) | Where-Object {$_.User.EndsWith("$") -and $_.IsGMSAAccount})) { + Test-OrInstallADServiceAccount $service.User + } +} diff --git a/Modules/Alkami.PowerShell.AD/Public/Test-OrInstallADServiceAccount.ps1 b/Modules/Alkami.PowerShell.AD/Public/Test-OrInstallADServiceAccount.ps1 new file mode 100644 index 0000000..1f39639 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/Public/Test-OrInstallADServiceAccount.ps1 @@ -0,0 +1,32 @@ +function Test-OrInstallADServiceAccount { +<# +.SYNOPSIS + Ensures gMSA Service Account specified exists on the machine. Creates it if not. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [string]$gmsaServiceAccount + ) + + $logLead = (Get-LogLeadName); + + # Get the actual username, since the AD functions error if the domain prefix is included + $cleanUserName = $gmsaServiceAccount.Split("\") | Select-Object -Last 1 + + if (Test-ADServiceAccount $cleanUserName) { + Write-Verbose ("$logLead : GMSA account {0} already installed" -f $cleanUserName) + return $true + } else { + Write-Verbose ("$logLead : Attempting to install GMSA account {0}" -f $cleanUserName) + Install-ADServiceAccount $cleanUserName + + if (Test-ADServiceAccount $cleanUserName) { + return $true + } + + Write-Warning ("$logLead : GMSA Account {0} could not be installed and must be reviewed post-installation" -f $cleanUserName) + return $false + } +} + diff --git a/Modules/Alkami.PowerShell.AD/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.AD/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.AD/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.AD/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.AD/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.nuspec b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.nuspec new file mode 100644 index 0000000..30f0497 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.nuspec @@ -0,0 +1,32 @@ + + + + Alkami.PowerShell.Choco + $version$ + Alkami Platform Modules - PowerShell - Choco + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.Choco + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Choco module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.psd1 b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.psd1 new file mode 100644 index 0000000..97fbb5d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.psd1 @@ -0,0 +1,12 @@ +@{ + RootModule = 'Alkami.PowerShell.Choco.psm1' + ModuleVersion = '3.31.4' + GUID = '5c35ea6e-7588-4111-8bea-e9e0604e920c' + Author = 'SRE' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing Chocolatey packages.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common', 'Alkami.PowerShell.Services', 'Alkami.PowerShell.AD' + FunctionsToExport = 'Compare-InstalledChocoPackages','Format-PackageTextWhitespace','Format-ParseChocoPackages','Get-AllInstalledComponentsByType','Get-BasicAuthHeader','Get-CategorizedChocoPackages','Get-ChocolateyParameterString','Get-ChocolateySources','Get-ChocolateySourcesV2','Get-ChocoPackageFromPath','Get-ChocoPublicPassThruFeedUrl','Get-ChocoState','Get-FriendlyChocoPackageName','Get-LocallyInstalledChocoPackages','Get-MicroservicesWithMigrations','Get-MicroserviceTiers','Get-PackageAlkamiManifestV2','Get-PackageAppConfigXml','Get-PackageFile','Get-PackageFileList','Get-PackageFileListV2','Get-PackageFileV2','Get-PackageInstallationData','Get-PackageMetadataV2','Get-PackageNuspecXmlV2','Get-PackageVersionsFromAlkamiProget','Get-RemoteInstalledChocoPackages','Get-ServicesByTier','Get-ValidHotfixServerTiers','Install-ExistingMicroservicesWithMigrations','Install-LegacyUtilityPackage','Install-ManualChocoPackages','Install-MicroservicesWithMigrations','Invoke-ChocoInstallPackages','Invoke-ChocoInstallPackagesPrivate','Invoke-ProgetRequest','Publish-ChocoPackage','Remove-OrphanedChocoFolders','Remove-PackagesThatAreAlreadyInstalled','Select-InstallPackages','Select-UniqueServerPackages','Set-ChocoPackageSourceFeeds','Set-ChocoPackageSourceFeedsV2','Set-ChocoPackageTags','Start-ServicesByTier','Test-ChocoPackagesAvailable','Test-IsChocoPackageInstalled','Test-IsEclairInstalled','Test-IsPackageComponentizedWebApplication','Test-IsPackageDbms','Test-IsPackageDbmsV2','Test-IsPackageFullScaleMicroserviceV2','Test-IsPackageInFeed','Test-IsPackageInfrastructureMicroserviceV2','Test-IsPackageInstaller','Test-IsPackageInstallerV2','Test-IsPackageMicroserviceV2','Test-IsPackagePowerShellModule','Test-IsPackagePowerShellModuleV2','Test-IsPackageReliableService','Test-IsPackageReliableServiceV2','Test-IsPackageUpgradeOnlyV2','Test-IsServiceManifestCore','Test-ManualChocoCommandsExecuted','Test-PackageHasAlkamiManifestV2','Test-PackageHasDatabaseConfigFile','Test-PackageHasDependencyV2','Test-PackageHasInfrastructureMigrationsV2','Test-ServiceManifestHasMigrations','Test-ServiceManifestRequiresDbAccess','Update-FeedAuthentication','Write-InstallPackageMetadataToConsole' +} diff --git a/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.pssproj b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.pssproj new file mode 100644 index 0000000..b9013e4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Alkami.PowerShell.Choco.pssproj @@ -0,0 +1,109 @@ + + + Debug + 2.0 + {1de98344-48dc-478c-bb71-9c75e93e6981} + Exe + MyApplication + MyApplication + Alkami.PowerShell.Choco + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.Choco") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Choco/AlkamiManifest.xml new file mode 100644 index 0000000..c7268a9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.Choco + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.Choco/Private/Get-ChocoStateReleaseInput.ps1 b/Modules/Alkami.PowerShell.Choco/Private/Get-ChocoStateReleaseInput.ps1 new file mode 100644 index 0000000..4bd7594 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Private/Get-ChocoStateReleaseInput.ps1 @@ -0,0 +1,35 @@ +function Get-ChocoStateReleaseInput { +<# +.SYNOPSIS + Returns a list of package name/version/feed objects from an unsanitized user input list of "package version"'s. +#> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + [Parameter(Mandatory = $true)] + [string]$text + ) + + $option = [System.StringSplitOptions]::RemoveEmptyEntries; + $lines = $text.Split([Environment]::NewLine, $option); + + $validPackages = @(); + $lines | ForEach-Object { + + # Cut down whitespace. + $line = ($_ -replace '\s+', ' ').Trim(); + $data = $line.Split(' ', $option); + + # Arbitrary but reasonably effective means of filtering for valid name/version text input lines. + if (($data.count -eq 2) -and ($data[1] -like "*.*")) { + $properties = @{ Name = $data[0]; Version = $data[1]; Feed = $null; Tags = $null; }; + $pkg = New-Object -TypeName PSObject -Prop $properties; + + $validPackages += $pkg; + } else { + write-error "$_ is not a valid package name and version" + } + }; + + return $validPackages; +} diff --git a/Modules/Alkami.PowerShell.Choco/Private/IsPackageWidget.ps1 b/Modules/Alkami.PowerShell.Choco/Private/IsPackageWidget.ps1 new file mode 100644 index 0000000..a8192b9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Private/IsPackageWidget.ps1 @@ -0,0 +1,48 @@ +function IsPackageWidget { +<# +.SYNOPSIS + Returns true if a given package name is a widget. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $true)] + [string]$packageName + ) + + # Match known Widget naming conventions. + $widgetNames = @("Alkami.Apps*", "Alkami.Client*", "Alkami.Modules*", "Alkami.WebExtensions*"); + foreach ($name in $widgetNames) { + if ($packageName -match $name) { + return $true; + } + } + + # Onto fallback methods if naive name matching doesn't work. + + # Parse tags from the verbose output of a choco search. + Write-Host "Pulling tags for package '$packageName'"; + $verboseOutput = (choco search -l -verbose -e $packageName); + + $tagLine = $verboseOutput | Where-Object { ($_.Trim().StartsWith("Tags:")) }; + if (!($tagLine)) { + return $false; + } + + $tagLine = $tagLine.Substring($tagLine.IndexOf(":") + 1); + $option = [System.StringSplitOptions]::RemoveEmptyEntries; + $tags = $tagLine.Split(' ', $option); + + if ($tags.count -eq 0) { + return $false; + } + + foreach ($tag in $tags) { + if ($tag.ToLower() -match "widget") { + return $true; + } + } + + # No widget tag found. + return $false; +} diff --git a/Modules/Alkami.PowerShell.Choco/Private/Test-ChocoPackagesAvailablePrivate.ps1 b/Modules/Alkami.PowerShell.Choco/Private/Test-ChocoPackagesAvailablePrivate.ps1 new file mode 100644 index 0000000..2a6b567 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Private/Test-ChocoPackagesAvailablePrivate.ps1 @@ -0,0 +1,81 @@ +function Test-ChocoPackagesAvailablePrivate { +<# +.SYNOPSIS + Verifies that a list of packages and their versions are available in the Chocolatey package feed. + Returns a delimited list of packages that were not found. i.e. Alkami.Package1|1.0.0,Alkami.Package2|1.0.1 + +.PARAMETER Packages + Array of Package Objects to Test Availability based on configured feeds + +.PARAMETER NugetCredential + Credential object for Nuget server +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [object[]]$Packages, + [Parameter(Mandatory = $true)] + [pscredential]$NugetCredential + ) + $logLead = (Get-logLeadName) + + Write-Host "$logLead : Verifying that all choco packages exist in the feed." + + # Assign package sources to each package. + # HACK: Currently, _this_ function is ONLY called by it's non-private counterpart Test-ChocoPackagesAvailable + # which is ONLY called by TDC's Test-ChcooPackagesAvailable.ps1 + # However, Set-ChocoPackageSourceFeeds is called by a LOT of other things, so it has + # a new Parameter "-MissingPackageLogLevel" that defaults to ERROR to maintain default behavior + # SRE-15611 needs this to not Write-Error for Missing Packages. That's the whole point of this + # call - to get a list of Missing Packages so that we can either AutoPromote them or + # use TeamCity's "buildProblem" outputs to fail while bubbling information up + Set-ChocoPackageSourceFeeds -Packages $packages -MissingPackageLogLevel "WARNING" + + $missingPackages = @() + $success = $true + $packages | ForEach-Object { + $foundCurrentPackage = $false + $name = $_.Name + $version = $_.Version + if ($null -eq $_.Feed) { + #badnews + Write-Warning "$logLead : Package: $name $version has NULL FEED" + } else { + # This was throwing a NPE when the Package was not assigned a Feed from Set-ChocoPackageSourceFeeds + $feed = ($_.feed.Source).TrimEnd('/') # Just in case the source has a trailing '/' + + try { + $authHeaders = @{ + Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($NugetCredential.UserName):$(Get-PasswordFromCredential $NugetCredential)")) + } + + $result = @(Invoke-WebRequest -Headers $authHeaders "$feed/Packages(Id='$name',Version='$version')" -ErrorAction SilentlyContinue) + if ($result.StatusCode -eq 200) { + Write-Host "$logLead : Package: $name $version found on Feed: $feed" + $foundCurrentPackage = $true + } else { + Write-Warning "$logLead : Package: $name $version not found on Feed: $feed" + } + } catch { + Write-Warning "$logLead : Package: $name $version not found on Feed: $feed" + } + } + + if (-not $foundCurrentPackage) { + $feedString = if ($_.Feed) { $feed } else { "NULL FEED" } + Write-Warning "$logLead : Package: $name $version not found on Feed: $feedString" + $missingPackages += $_ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification = "Variable assignment is used further down in IF statement. False positive.")] + $success = $false + } + } + + if ($success) { + Write-Host "$logLead : All Chocolatey packages are available in the feed." + } else { + Write-Host "$logLead : There are required Chocolatey package(s) that are unavailable in the Chocolatey feed!" + Write-Host $missingPackages + + return (($missingPackages | ForEach-Object { "$($_.Name)|$($_.Version)" }) -join ",") + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.Choco/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..84b15c9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Private/VariableDeclarations.ps1 @@ -0,0 +1,155 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +[HashTable[]]$installedChocolateyPackages = @(); +[HashTable[]]$chocolateySources = @(); + +# Microservice Tiers +$_MicroserviceTiers = @( @( + "Alkami.Services.Subscriptions.Host", + "Alkami.MicroServices.Broker.Host" + ), @( + "Alkami.MicroServices.Forms.Service.Host", + "Alkami.MicroServices.Authorization.Service.Host", + "Alkami.MicroServices.Security.Service.Host", + "Alkami.MicroServices.UserInterface.Service.Host", + "Alkami.MicroServices.Audit.Service.Host", + "Alkami.MicroServices.Contacts.Service.Host", + "Alkami.MicroServices.Holidays.Service.Host", + "Alkami.MicroServices.Images.Service.Host", + "Alkami.MicroServices.Notifications.Service.Host", + "Alkami.MicroServices.Settings.Service.Host", + "Alkami.MicroServices.SiteText.Service.Host", + "Alkami.MicroServices.Transactions.Service.Host", + "Alkami.MicroServices.EventManagement.Service.Host", + "Alkami.MS.DataEngineSettings.Service.Host", + "Alkami.MS.MessageSubscriptions.Host" + ) +); + +# Define infrastructure microservices that must be running on each server. +$_InfrastructureMicroservices = @( + "Alkami.Services.Subscriptions.Host", + "Alkami.MicroServices.Broker.Host", + "Alkami.MicroServices.Features.Beacon.Host", + "newrelic-dotnet" +) + +# Define microservices that need to run on every single FAB server. +# This is different from infrastructure microservices because they are not deployed to web/app servers. +$_FullScaleMicroservicesAllowList = @( + "Alkami.MicroServices.Authorization.Service.Host", + "Alkami.MicroServices.Accounts.Service.Host", + "Alkami.MicroServices.Transactions.Service.Host", + "Alkami.MicroServices.Security.Service.Host", + "Alkami.MicroServices.Settings.Service.Host" +) +$_FullScaleMicroservicesAllowList += $_InfrastructureMicroServices + +# Defines packages to be summarily considered microservices. +$_MicroserviceAllowList = @( + "*.MicroServices.*", + "*.MS.*", + "*.MicroService.*", + "*.Services.*", + "fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host" +) + +# Defines packages to be excluded from microservices. +$_MicroserviceDenyList = @() + +# Define packages to be summarily considered Web Tier packages. +$_WebAllowList = @( + "Alkami.OpenSource.License.Listing", + "Alkami.Utilities.WebToolKit.Snippets.Runtime", + "DS.Repository.Common", + "DS.Repository.MicroserviceClient", + "DSFCU.MessageRepository.Implementation", + "DS.CopyRequest.Widget", + "Patelco.Widget.OpenAccountLegacy" +) + +# Define packages to be summarily considered App Tier packages. +$_AppAllowList = @( + "Alkami.CoreDashboard", + "Alkami.Security.RPSTS", + "Alkami.VisaAuthorizationsConverter", + "Alkami.App.Providers.Core.ESB.OCCU", + "Alkami.App.Providers.eStatements.SandiaLabs", + "Alkami.App.Providers.eStatements.SandiaLabsSso", + "Achieva.App.Provider.Rewards", + "Alkami.App.Providers.SymConnectMultiplexer", + "Alkami.Utilities.NagViewer", + "Alkami.Console.Dynamodb.Migrater", + "Alkami.MicroServiceTester", + "Alkami.Tools.Deconversion", + "Alkami.Broker.Testing.Console" +) + +# Define packages to be placed on the App Tier in FAB Environments Only +$_AppAllowListFabOnly = @( + "*Sym*", + "Alkami.MicroServices.RemoteDepositProviders.ProfitStars.Onus.Service.Host" +) + +# Define nuspec tags for Web Tier packages. +$_WebNuspecTags = @("Widget","AdminWidget","Admin","WebApplication","WebExtension", "Repository", "Module", "Modules", "Website", "Snippet"); + +# Define nuspec tags for App Tier packages. +$_AppNuspecTags = @("Provider","FileProcessing"); + +# Define installer packages that must be installed prior to other packages. +$_InstallerPackages = @( + "*Choco.Installer*", + "*Alkami.Installer*", + "Alkami.TI.TechnicalImplementations", + "chocolatey", + "Carbon", + "Pester" +) + +# Define PowerShell module/cmdlet packages. +$_PowerShellModulePackages = @( + "Alkami.PowerShell.*", + "Alkami.DevOps.*", + "Alkami.Ops.*", + "SRE.MigrationUtility" +) + +# Define packages that must only be installed in upgrade-mode. +$_UpgradePackages = @( + "chocolatey", + "Carbon", + "debugdiagnostic", + "Pester", + "Alkami.Installer.*", + "Alkami.PowerShell.*", + "Alkami.DevOps.*", + "Alkami.Ops.*", + "Alkami.CoreDashboard", + "Alkami.EagleEye", + "newrelic-dotnet" +) + +# Define package dependencies that signify database usage / dbms user requirements. +$_DbmsDependencies = @( + "Alkami.MicroServices.Choco.Installer.Database", + "Alkami.MicroServices.Choco.Installer.MasterDatabase" +) + +# Define package dependencies that signify that a package is a microservice. +$_MicroDependencies = @( + "Alkami.MicroServices.Choco.Installer.Logic", + "Alkami.MicroServices.Choco.Installer.Database", + "Alkami.MicroServices.Choco.Installer.MasterDatabase" +) + +# "Legacy" WebApplications - these need to sort higher than others in the same tier +# because they live in `Get-OrbPath` and other packages might try to install into +# one of their subfolders; these packages need to install prior to others +# in the same tier +$_ComponentizedWebApplications = @( + "Alkami.Security.RPSTS", + "Alkami.App.Service.Scheduler.Host", + "Alkami.App.Providers.SymConnectMultiplexer" +) \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.Tests.ps1 new file mode 100644 index 0000000..f350569 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.Tests.ps1 @@ -0,0 +1,137 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Compare-InstalledChocoPackages" { + + Mock Get-RemoteInstalledChocoPackages -ModuleName $moduleForMock { + + [hashtable]$hostobjectProperty = @{ } + $hostobjectProperty.Add('foo.fh.local', + @("expectedPackage|1.1.1", + "MissingFromTargetPackage|3.3.3", + "OutOfOrderexpectedPackage|1.1.2", + "expectedPackageTargetVersionDiff|6.6.6", + "expectedPackageSourceVersionDiff|2.0.1" + ) + ) + $hostobjectProperty.Add('PSComputerName', "foo.fh.local") + $hostobjectProperty.Add("RunspaceId", "1c40b61f-fake-4f6e-bf42-1ff77d9e7e19") + $hostobjectProperty.Add('PSShowComputerName', $true) + + [hashtable]$hostobjectProperty2 = @{ } + $hostobjectProperty2.Add('PSComputerName', "bar.fh.local") + $hostobjectProperty2.Add("RunspaceId", "1c40b61f-fake-4f6e-bf42-1ff77d9e7e19") + $hostobjectProperty2.Add('PSShowComputerName', $true) + $hostobjectProperty2.Add('bar.fh.local', + @("Chocolatey v0.10.11", + "expectedPackage|1.1.1", + "missingFromSourcePackage|2.2.2", + "expectedPackageTargetVersionDiff|9.9.9", + "expectedPackageSourceVersionDiff|0.0.1", + "OutOfOrderexpectedPackage|1.1.2" + ) + ) + + $mockedDict = @() + $mockedDict += @($hostobjectProperty) + $mockedDict += @($hostobjectProperty2) + $mockeddict | convertto-json | Write-Host + + return $mockedDict + } + + $mockedResults = Compare-InstalledChocoPackages -sourceServer foo.fh.local -targetServer bar.fh.local + $mockedResults | convertto-json | Write-Host + Context "Full Match" { + BeforeEach { + $expectedPackageName = 'expectedPackage' + $OutOfOrderexpectedPackageName = "OutOfOrderexpectedPackage" + } + It "Identical results returned in `$fullyMatchedPackages" { + $mockedResults.fullyMatchedPackages.Name | Should -contain $expectedPackageName + } + It "Identical results not returned in `$missingFromSource" { + $mockedResults.missingFromSource.Name | Should -not -Contain $expectedPackageName + } + It "Identical results not returned in `$missingFromTarget" { + $mockedResults.missingFromTarget.Name | Should -not -Contain $expectedPackageName + } + It "Identical results not returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.Name | Should -not -Contain $expectedPackageName + } + It "Out of order full match results returned in `$fullyMatchedPackages" { + $mockedResults.fullyMatchedPackages.Name | Should -Contain $OutOfOrderexpectedPackageName + } + It "Out of order results not returned in `$missingFromSource" { + $mockedResults.missingFromSource.Name | Should -not -Contain $OutOfOrderexpectedPackageName + } + It "Out of order results not returned in `$missingFromTarget" { + $mockedResults.missingFromTarget.Name | Should -not -Contain $OutOfOrderexpectedPackageName + } + It "Out of order results not returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.Name | Should -not -Contain $OutOfOrderexpectedPackageName + } + } + Context "Missing from Source" { + BeforeEach { + $expectedPackageName = 'missingFromSourcePackage' + } + It "Target has entry not in source - returned in `$missingFromSource" { + $mockedResults.missingFromSource.Name | Should -contain $expectedPackageName + } + It "Target has entry not in source - not returned in `$fullyMatchedPackages" { + $mockedResults.fullyMatchedPackages.Name | Should -not -contain $expectedPackageName + } + It "Target has entry not in source - not returned in `$missingFromTarget" { + $mockedResults.missingFromTarget.Name | Should -not -contain $expectedPackageName + } + It "Target has entry not in source - not returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.Name | Should -not -contain $expectedPackageName + } + + } + Context "Missing from Target" { + BeforeEach { + $expectedPackageName = 'MissingFromTargetPackage' + } + It "Source has entry not in target - returned in `$missingFromTarget" { + $mockedResults.missingFromTarget.Name | Should -contain $expectedPackageName + } + It "Source has entry not in target - not returned in `$fullyMatchedPackages" { + $mockedResults.fullyMatchedPackages.Name | Should -not -contain $expectedPackageName + } + It "Source has entry not in target - not returned in `$missingFromSource" { + $mockedResults.missingFromSource.Name | Should -not -contain $expectedPackageName + } + It "Source has entry not in target - not returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.Name | Should -not -contain $expectedPackageName + } + } + Context "Version mismatch" { + BeforeEach { + $expectedPackageNameTargetVersionDiff = 'expectedPackageTargetVersionDiff' + $expectedPackageNameSourceVersionDiff = 'expectedPackageSourceVersionDiff' + } + It "Package Name matches but Target version differs - returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.name | Should -Contain $expectedPackageNameTargetVersionDiff + } + It "Package Name matches but Source version differs - returned in `$packageVersionMismatch" { + $mockedResults.packageVersionMismatch.name | Should -Contain $expectedPackageNameSourceVersionDiff + } + It "Package Name matches but Target version differs - not returned in`$missingFromTarget" { + $mockedResults.missingFromTarget.name | Should -not -Contain $expectedPackageNameTargetVersionDiff + } + It "Package Name matches but Target version differs - not returned in `$missingFromSource" { + $mockedResults.missingFromSource.name | Should -not -Contain $expectedPackageNameTargetVersionDiff + } + It "Package Name matches but Target version differs - not returned in `$fullyMatchedPackages" { + $mockedResults.fullyMatchedPackages.Name | Should -not -contain $expectedPackageNameTargetVersionDiff + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.ps1 new file mode 100644 index 0000000..a45477a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Compare-InstalledChocoPackages.ps1 @@ -0,0 +1,127 @@ +function Compare-InstalledChocoPackages { + <# +.SYNOPSIS + This function compares installed choco packages between servers + +.DESCRIPTION + Invoke Compare-InstalledChocoPackages -SourceServer foo.fh.local -targetServer bar.fh.local + The function exposes these properties describing comparisons: + .fullyMatchedPackages + .missingFromSource + .missingFromTarget + .packageVersionMismatch + .packageVersionMismatchDetails + +.EXAMPLE + Compare-InstalledChocoPackages -SourceServer foo.fh.local -targetServer bar.fh.local + Compare the installed choco packages and determines similarities or differences + +.INPUTS + -SourceServer foo.fh.local -targetServer bar.fh.local + +.OUTPUTS + .fullyMatchedPackages + .missingFromSource + .missingFromTarget + .packageVersionMismatch + .packageVersionMismatchDetails + +#> + [CmdletBinding()] + param ( + $SourceServer = ([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname, + $TargetServer + ) + $logLead = Get-LogLeadName + $providerStopWatch = [System.Diagnostics.StopWatch]::StartNew() + + # go get package list + $serversToQuery = @($SourceServer, $TargetServer) + $results = Get-RemoteInstalledChocoPackages -serversToQuery $serversToQuery + + # assign first result to source and second to target. order is based on index in $sessions + foreach ($result in $results) { + if ($result.pscomputername -eq $serversToQuery[0]) { + $sourcePackages = ($result[$serversToQuery[0]]) + } + if ($result.pscomputername -eq $serversToQuery[1]) { + $targetPackages = ($result[$serversToQuery[1]]) + } + } + # select packages that match in both name and version + $fullyMatchedPackages = @() + foreach ($sourcepackage in $sourcePackages) { + if ($targetPackages.contains($sourcepackage)) { + $fullyMatchedPackages += $sourcepackage + } + } + + Write-Verbose "$logLead : Packages matching between $SourceServer & $TargetServer" + + # remove the versions from packages to make use of .contains method + $targetPackageNames = @() + foreach ($targetPackage in $targetPackages) { + $targetPackageNames += $targetPackage.Split('|')[0] + } + # find packages entirely missing from target + $missingFromTarget = @() + foreach ($sourcepackage in $sourcePackages) { + if (!$targetPackageNames.contains($sourcepackage.Split('|')[0])) { + $missingFromTarget += $sourcepackage + } + } + + Write-Verbose "$logLead : Packages not installed on target machine $TargetServer" + + + # find packages entirely missing from source but exist on target + $sourcePackageNames = @() + foreach ($sourcePackage in $sourcePackages) { + $sourcePackageNames += $sourcePackage.Split('|')[0] + } + + $missingFromSource = @() + foreach ($targetPackage in $targetPackages) { + if (!$sourcePackageNames.contains($targetPackage.Split('|')[0])) { + $missingFromSource += $targetPackage + } + } + + Write-Verbose "$logLead : Packages not installed on source machine $SourceServer" + + # find packages that exist on source and target but may be version mismatched + # recording both package name as well as detailed diff source -> target + $nameMatchedPackages = @() + $packageVersionMismatchDetails = @() + $packageVersionMismatch = @() + foreach ($sourcePackage in $sourcePackages) { + if ($targetPackageNames.contains($sourcepackage.Split('|')[0])) { + $nameMatchedPackages += $sourcePackage + } + } + + foreach ($nameMatchedPackage in $nameMatchedPackages) { + if (!$targetPackages.Contains($nameMatchedPackage)) { + $packageVersionMismatch += $nameMatchedPackage.Split('|')[0] + $packageVersionMismatchDetails += ( $nameMatchedPackage + " -> " + ($targetPackages -like ( "$($nameMatchedPackage.Split('|')[0])|*" ) ) ) + } + } + + $fullyMatchedPackages = Format-ParseChocoPackages $fullyMatchedPackages + $missingFromSource = Format-ParseChocoPackages $missingFromSource + $missingFromTarget = Format-ParseChocoPackages $missingFromTarget + $packageVersionMismatch = Format-ParseChocoPackages $packageVersionMismatch + + # organize comparisons done above into dict + $comparedPackageLists = @{ } + $comparedPackageLists += @{ fullyMatchedPackages = $fullyMatchedPackages } + $comparedPackageLists += @{ missingFromSource = $missingFromSource } + $comparedPackageLists += @{ missingFromTarget = $missingFromTarget } + $comparedPackageLists += @{ packageVersionMismatch = $packageVersionMismatch } + $comparedPackageLists += @{ packageVersionMismatchDetails = $packageVersionMismatchDetails } + + + Write-Verbose "$logLead : [$($providerStopWatch.Elapsed)] : comparisons complete" + $providerStopWatch.Stop() + return $comparedPackageLists +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.Tests.ps1 new file mode 100644 index 0000000..221a9c0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.Tests.ps1 @@ -0,0 +1,72 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Format-PackageTextWhitespace" { + + Context "When Called With Endlines" { + + $myFakePackageString = @" +Alkami.Services.SRE.Blah 1.0.1 +Alkami.Services.SRE.Legacy.FooBar 1.0.1 +"@ + + $resultingPackageString = Format-PackageTextWhitespace $myFakePackageString + + It "Removes Endlines"{ + $resultingPackageString.Contains("\r\n") | Should -BeFalse + } + + It "Leaves in Single Spaces"{ + $resultingPackageString.Contains(" ") | Should -BeTrue + } + } + + Context "When Called With Multiple Spaces"{ + + $myFakePackageString = "Alkami.Services.SRE.Blah 1.0.1" + + $resultingPackageString = Format-PackageTextWhitespace $myFakePackageString + + It "Removes Multiple Spaces"{ + $resultingPackageString.Contains(" ") | Should -BeFalse + } + + It "Leaves in Single Spaces"{ + $resultingPackageString.Contains(" ") | Should -BeTrue + } + } + + Context "When Called With Tabs" { + $myFakePackageString = "Alkami.Services.SRE.Blah`t1.0.1`tAlkami.Services.SRE.Legacy.FooBar`t1.0.1" + + $resultingPackageString = Format-PackageTextWhitespace $myFakePackageString + + It "Removes tabs"{ + $resultingPackageString.Contains("`t") | Should -BeFalse + } + + It "Leaves in Single Spaces" { + $resultingPackageString.Contains(" ") | Should -BeTrue + } + } + + Context "When Called With Tabs And Spaces" { + + $myFakePackageString = "Alkami.Services.SRE.Blah `t 1.0.1 `t Alkami.Services.SRE.Legacy.FooBar `t 1.0.1" + + $resultingPackageString = Format-PackageTextWhitespace $myFakePackageString + + It "Removes tabs"{ + $resultingPackageString.Contains("`t") | Should -BeFalse + } + + It "Leaves in Single Spaces" { + $resultingPackageString.Contains(" ") | Should -BeTrue + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.ps1 new file mode 100644 index 0000000..a9ddae2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Format-PackageTextWhitespace.ps1 @@ -0,0 +1,23 @@ +function Format-PackageTextWhitespace { +<# +.SYNOPSIS + Takes in a list of packages and replaces any/all whitespace with a single space instead. +.PARAMETER PackageText + List of packages, generally from a deploy job. Expected to be comma separated. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $PackageText + ) + # Take all of the endlines and put a placeholder in. + $PackageText = ($PackageText -replace ("\r\n", "%")) + # If this is running from powershell (ie: local testing) strings won't have \r\n, they'll have `n. Preserve that. + $PackageText = ($PackageText -replace ("`n", "%")) + # Shrink extra whitespace down to a single space + $PackageText = ($PackageText -replace ("\s+", " ")) + # Put the endline back + $PackageText = ($PackageText -replace ("%", "`n")) + $PackageText = $PackageText.Trim() + return $PackageText +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.Tests.ps1 new file mode 100644 index 0000000..f9ed045 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.Tests.ps1 @@ -0,0 +1,210 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Format-ParseChocoPackages" { + + # Global Mocks + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + + Context "Positive Assertions" { + + It "Test single package with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1") -Validate + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + } + + It "Test single package" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "v1.7.1.1.1.1.1.1.1.1-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1") + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + } + + It "Test single package no version with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $properties1 = @{ Name = $packageName1; Version = $null; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + $arrayList = Format-ParseChocoPackages -text @("$packageName1") -Validate + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + } + + It "Test single package no version" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $properties1 = @{ Name = $packageName1; Version = $null; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + $arrayList = Format-ParseChocoPackages -text @("$packageName1") + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + } + + It "Test two packages, with same name and version, with validation, are deduplicated" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1-pre100" + $packageName2 = "alkami.api.afx" + $packageVersion2 = "1.7.1-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") -Validate + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + $arrayList[1].Name | Should -Be $null + $arrayList[1].Version | Should -Be $null + } + + It "Test two packages, same name and version" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "v1.7.1.0-pre100" + $packageName2 = "alkami.api.afx" + $packageVersion2 = "v1.7.1.0-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + $arrayList[1].Name | Should -Be $packages[1].Name + $arrayList[1].Version | Should -Be $packages[1].Version + } + + It "Test two packages, different name and versions with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.aufx" + $packageVersion1 = "1.4.0" + $packageName2 = "alkami.api.cfx" + $packageVersion2 = "1.7.1-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") -Validate + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + $arrayList[1].Name | Should -Be $packages[1].Name + $arrayList[1].Version | Should -Be $packages[1].Version + } + + It "Test two packages, different name and versions" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.cufx" + $packageVersion1 = "1.4.0.0" + $packageName2 = "alkami.api.afx" + $packageVersion2 = "1.7.1.0-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + $arrayList = Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") + $arrayList[0].Name | Should -Be $packages[0].Name + $arrayList[0].Version | Should -Be $packages[0].Version + $arrayList[1].Name | Should -Be $packages[1].Name + $arrayList[1].Version | Should -Be $packages[1].Version + } + + It "Test same package name, different versions without validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1.0-pre100" + $packageName2 = "alkami.api.afx" + $packageVersion2 = "1.7.1.0-pre200" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + {Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") -ErrorAction Stop} | Should Not Throw + } + + It "Test Version number trailing decimal without validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1.1.-pre10" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + {Format-ParseChocoPackages -text @("$packagename1|$packageVersion1") -ErrorAction Stop} | Should Not Throw + } + } + + Context "Negative Assertions" { + It "Test same package name, different versions with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1-pre100" + $packageName2 = "alkami.api.afx" + $packageVersion2 = "1.7.1-pre200" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $properties2 = @{ Name = $packageName2; Version = $packageVersion2; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $pkg2 = New-Object -TypeName PSObject -Prop $properties2; + $packages.Add($pkg1) + $packages.Add($pkg2) + {Format-ParseChocoPackages -text @("$packageName1|$packageVersion1","$packageName2|$packageVersion2") -Validate -ErrorAction Stop} | Should Throw + } + + It "Test malformed package version with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.-pre100" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + {Format-ParseChocoPackages -text @("$packagename1|$packageVersion1") -Validate -ErrorAction Stop} | Should Throw + } + + It "Test Version number too many positions with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7.1.1" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + {Format-ParseChocoPackages -text @("$packagename1|$packageVersion1") -Validate -ErrorAction Stop} | Should Throw + } + + It "Test Version number too few positions with validation" { + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.api.afx" + $packageVersion1 = "1.7" + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + {Format-ParseChocoPackages -text @("$packagename1|$packageVersion1") -Validate -ErrorAction Stop} | Should Throw + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.ps1 new file mode 100644 index 0000000..4eeb188 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Format-ParseChocoPackages.ps1 @@ -0,0 +1,106 @@ +function Format-ParseChocoPackages { +<# +.SYNOPSIS + Returns a list of package objects parsed from choco-style "name|version" strings. + +.DESCRIPTION + Takes a list of package names and optional versions and returns array of PSObjects with properties containing + the Name and Version (if entered) parsed. + +.PARAMETER Text + Can be either an array of package names with versions to parse or a NewLine-delimited string of + package names and versions. + +.PARAMETER Delimiter + Delimiter to use when splitting the package name from the version in the $Text parameter. Defaults to "|" + +.PARAMETER Validate + Switch to use SemVer regular expression validation on the Version. Also performs duplicate check on 'PackageName but different Version' passed into -Text parameter. + +.Example + Format-ParseChocoPackages -Text @("alkami.api.afx|1.7.1-pre100","alkami.api.afx|1.7.1-pre100") -Validate + Format-ParseChocoPackages -Text alkami.api.afx|1.7.1 -Validate + Format-ParseChocoPackages -Text alkami.api.afx + Format-ParseChocoPackages -Text chocolatey|v1.7.1.0 +#> + [CmdletBinding()] + [OutputType([System.Object])] + [OutputType([System.Collections.ArrayList])] + Param( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object]$Text, + [Parameter(Mandatory = $false)] + [string]$Delimiter = "|", + [Parameter(Mandatory = $false)] + [switch]$Validate + ) + + if (!$Text) { + $packages = @() + return $packages + } + + $logLead = (Get-LogLeadName) + + # Detect if it's a single string passed in and split it into a string-array per line. + if(!($Text -is [array])) { + $Text = $Text.Split([Environment]::NewLine) + } + + # Detect any exact duplicate lines and remove them. + # If this is coming from Classify-Packages, we've already trimmed this. But if it isn't, we still want this unique to work as expected. + if($Validate){ + $Text = $Text.Trim() | Sort-Object | Get-Unique + } + + Write-Verbose "$logLead : Parsing $($Text.Count) choco packages..." + $chocoPackages = [System.Collections.ArrayList]::new() + $packageMap = @{}; + $Text | ForEach-Object { + if(!([string]::IsNullOrEmpty($_))) { + + Write-Verbose "$logLead : Parsing $_" + $option = [System.StringSplitOptions]::RemoveEmptyEntries + $splitItem = $_.Trim().Split($Delimiter, $option) + + Write-Verbose "$logLead : Package Name parsed to [$($splitItem[0])]" + Write-Verbose "$logLead : Package Version parsed to [$($splitItem[1])]" + + if($Validate -and $splitItem.Count -gt 2) { + Write-Error "Package [$($_.Trim())] passed in is not in the correct SemVer3 format. Is there more than one package on a line?" + } + + # Make sure that the version isn't malformed. Should be in the proper Semver format. Regular Epxression taken from https://semver.org + if($Validate -and $null -ne $splitItem[1] -and $splitItem[1] -cnotmatch "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") { + + Write-Error "Version [$($splitItem[1])] of module [$($splitItem[0])] passed in is not in the correct SemVer3 format." + } + + $properties = @{ Name = $splitItem[0]; Version = $splitItem[1]; Feed = $null; Tags = $null; IsService = $null; StartMode = $null; IsValid = $false;} + $pkg = New-Object -TypeName PSObject -Prop $properties + + # Test if the package object is already in the array. Return error if same package name but different versions are in the input array. + if($Validate) { + + $lowerName = $pkg.Name.ToLower() + + if($packageMap.ContainsKey($lowerName)) { + + if($packageMap[$lowerName] -ne $pkg.Version) { + Write-Error "Package [$($pkg.Name)] is in the list more than once with differing versions. Please review your package parameters and include only the correct version" + Write-Host "##teamcity[buildProblem description='Multiple versions of $($pkg.Name) are in the install list' identity='$($pkg.Name)']" + } + } + + $packageMap[$lowerName] = $pkg.Version + } + + $chocoPackages.Add($pkg) | Out-Null + } + } + + Write-Verbose "$logLead : Successfully parsed $($chocoPackages.Count) packages." + + return $chocoPackages +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.Tests.ps1 new file mode 100644 index 0000000..281fdb4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.Tests.ps1 @@ -0,0 +1,52 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AllInstalledComponentsByType" -Tag "Integration" { + # Look in the parent folder of $here (defined above as the current folder) in the sibling folder "TestFiles" + $global:fakeFolderPath = (Join-Path -Path (Split-Path $here -Parent) -ChildPath "TestFiles") + Mock -Module $moduleForMock -Command Get-ChocolateyInstallPath -MockWith { $global:fakeFolderPath } + Mock -Module $moduleForMock -Command Get-LogLeadName -MockWith { return "UUT" } + Mock -Module $moduleForMock -Command Invoke-Parallel2 -MockWith { + # Because PowerShell is smarter than we are, it puts the arguments into the specifically named values + # I expected it to give me param ($arg1, $arg2, etc) but ... sigh, no. + # So there is no parameter filter, because I know what I'm looking for, so I just go for that one. + # If we add more than one Widget to our test-cases, we should change the response here. + param ($objects, $arguments) +# For future us trying to remember how these things are used in tests ... +# Write-Host $objects +# Write-Host $arguments + $packageProperties = @{ Result = @{ + PackageName = "TestWidget" + ManifestPath = $objects.Where({$PSItem -match "Widget"}) + Manifest = @{ general = @{} } + }} + return (New-Object -TypeName PSObject -Property $packageProperties) + } + + Context "Integration test returns one record for Widget" { + It "Does not throw" { + { Get-AllInstalledComponentsByType -ComparableComponentType 'Widget' } | Should -Not -Throw + } + + $result = (Get-AllInstalledComponentsByType -ComparableComponentType 'Widget') + + It "Result is not null" { + $result | Should -Not -BeNullOrEmpty + } + + It "Has one record" { + $result | Should -HaveCount 1 + } + + It "Has the key TestWidget" { + $result.PackageName | Should -Be "TestWidget" + } + } + + Remove-Variable -Scope global -Name fakeFolderPath -Force +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.ps1 new file mode 100644 index 0000000..f3f6db7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-AllInstalledComponentsByType.ps1 @@ -0,0 +1,111 @@ +function Get-AllInstalledComponentsByType { +<# +.SYNOPSIS + Get all the packages with manifests on this server which have the specified ComponentType + +.PARAMETER ComparableComponentType + The type of component to compare against + +.OUTPUTS + This function returns a list of objects that has the PackageName, Manifest path, and Manifest contents for any manifest in the known package locations folders for any matching manifest name pattern where the requested component type matches. +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet("Widget","Provider","WebExtension","WebApplication","Service","Hotfix")] + [Alias('ComponentType')] + [string]$ComparableComponentType + ) + + $logLead = (Get-LogLeadName) + $allPackages = @() + + # Used to look up packages in parallel + $sb_manifestGather = { + param ($sb_manifestLocation, $sb_arguments) + $sb_routedComponentType, $sb_logLead = $sb_arguments + + if ([string]::IsNullOrWhiteSpace($sb_manifestLocation)) { + return + } + + Write-Verbose "$sb_logLead : Checking [$sb_manifestLocation] for type [$sb_routedComponentType]" + + $manifest = (Get-PackageManifest -Path $sb_manifestLocation -SkipTests) #skip tests because you can't be an installed package with a bad manifest, right? + $componentType = $manifest.general.componentType + if ($componentType -eq $sb_routedComponentType) { + $packageName = (Get-ChocoPackageFromPath $sb_manifestLocation) + $packageProperties = @{ + PackageName = $packageName + ManifestPath = $sb_manifestLocation + Manifest = $manifest + } + return (New-Object -TypeName PSObject -Property $packageProperties) + } + } + + # Due to a quirk, Service was keyed as Alkami.Installer.Services and not caught early enough + # This is considered acceptable because migrations also need a secondary path option for this routing + + $RoutedComponentType = $ComparableComponentType + + if ($ComparableComponentType -eq "Service") { + $RoutedComponentType = "Services" + } + + $chocoPath = (Get-ChocolateyInstallPath) + $validManifestFilenames = (Get-ValidPackageManifestFilenames) + + $manifestLocations = @() + foreach ($validManifestFilename in $validManifestFilenames) { + $manifestLocations += (Get-ChildItem -Path $chocoPath -Recurse -Filter $validManifestFilename -File).FullName + } + + if (Test-IsCollectionNullOrEmpty $manifestLocations) { + Write-Host "$logLead : Could not find any manifest files under [$chocoPath]. Are manifest packages installed? (They aren't on new server standups)" + return $allPackages + } + + $packageResults = @() + # Grab the .Result because of the way IP2 works + $packageResults += (Invoke-Parallel2 -Objects $manifestLocations -Arguments @($RoutedComponentType, $logLead) -Script $sb_manifestGather).Result + if (Test-IsCollectionNullOrEmpty $packageResults) { + Write-Host "$logLead : Could not find any manifest files with the specified type [$ComparableComponentType] from [$($manifestLocations.Count)] manifests in [$chocoPath]" + return $allPackages + } + + # Remove (SDK components + widgets) /src (or other sub) folders because those also contain a secondary manifest underneath + $packageMap = @{} + $manifestMap = @{} + foreach ($package in $packageResults) { + if ($null -eq $package) { + continue + } + $manifestMap[$package.ManifestPath] = $package.Manifest + # Two conditions: + # The value in the map is currently null, so we store it + # The value we already have in the map is _longer_ than the one we are staring at, so let's replace it with the shorter path. + # Shorter paths are the paths in the roots, which is what we want. + # re: -gt => If they are the same length, then it's probably the same file, which idk, it's fine + if ($null -ne $packageMap[$package.PackageName]) { + if ($packageMap[$package.PackageName].Length -gt $package.ManifestPath.Length) { + Write-Host "$logLead : Replacing [$($packageMap[$package.PackageName])] with [$($package.ManifestPath)]" + $packageMap[$package.PackageName] = $package.ManifestPath + } else { + Write-Host "$logLead : Discarding candidate path [$($package.ManifestPath)] from consideration as it is superceded by a manifest with a shorter path in the same parent path [$($packageMap[$package.PackageName])]" + } + } else { + $packageMap[$package.PackageName] = $package.ManifestPath + } + } + + # Put our final objects in allPackages + foreach ($key in $packageMap.Keys) { + $allPackages += (New-Object -TypeName PSObject -Property @{ PackageName = $key; ManifestPath = $packageMap[$key]; Manifest = $manifestMap[$packageMap[$key]] }) + } + + Write-Host "$logLead : Found [$($allPackages.Count)] packages with [$ComparableComponentType]" + + return $allPackages +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-BasicAuthHeader.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-BasicAuthHeader.ps1 new file mode 100644 index 0000000..a2be6e1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-BasicAuthHeader.ps1 @@ -0,0 +1,54 @@ +function Get-BasicAuthHeader { +<# +.SYNOPSIS + Turns a PSCredential into a basic auth header for usage with Invoke-WebRequest + +.DESCRIPTION + Turns a PSCredential into a basic auth header for usage with Invoke-WebRequest + When given a credential object, returns an object with a property Authorization and value set accordingly + When given no credential object, returns a $null value + +.PARAMETER Credential + [PSCredential] Optional parameter - When omitted will return a null string. + +.EXAMPLE + Get-BasicAuthHeader + +> Get-BasicAuthHeader +< $null + +.EXAMPLE + Get-BasicAuthHeader -Credential $credential + +> $credential = (Get-AlkamiCredential "test" "test") +> Get-BasicAuthHeader $credential +< @{ Authorization = "Basic dGVzdDp0ZXN0" } +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Mandatory=$false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName) + + if($null -eq $Credential) { + Write-Verbose "$loglead : No credential specified. Returning empty object." + return @{} + } + + Write-Verbose "$loglead : Credential specified. Building basic authentication header for user $($Credential.UserName)" + + ## Encode credentials into a basic auth header. "$($user):$($pass)"; + ## Basic Authorization header are base64 encoded credential pairs + $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes( + ("{0}:{1}" -f $Credential.UserName, (Get-PasswordFromCredential $Credential)) + )) + + $basicAuthValue = "Basic $encodedCreds" + $header = @{ + Authorization = $basicAuthValue + } + return $header +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-CategorizedChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-CategorizedChocoPackages.ps1 new file mode 100644 index 0000000..a5eb36f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-CategorizedChocoPackages.ps1 @@ -0,0 +1,166 @@ +function Get-CategorizedChocoPackages { +<# +.SYNOPSIS + Returns a list of categorized chocolatey packages by 'migrations', 'new', or 'other'. +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Mandatory=$false)] + [string]$packagesToInstallInput = $null, + [Parameter(Mandatory=$false)] + [switch]$includeFeeds + ) + + # Temporary fix. Add overrides to force certain packages to be treated like dbms accounts. + # This should be used when a microservice changes from a micro to a dbms account, which breaks service-account migration determination logic. + $migrationPackageOverrides = @( + "Alkami.MicroServices.Aggregation.Service.Host", + "Alkami.MicroServices.CardManagement.Service.Host", + "Alkami.MicroServices.AggregationProviders.Yodlee.Host", + "Alkami.MicroServices.Risk.Alkami.Service.Host", + "Alkami.MicroServices.SymConnectMultiplexer.Service.Host" + ) + + $loglead = (Get-LogLeadName); + + Write-Host "$loglead Getting list of locally installed chocolatey packages." + $localChocos = Get-ChocoState -l + + Write-Host "`n$loglead Reading feeds for local choco packages, and discarding default/internal tools choco packages." + Set-ChocoPackageSourceFeeds $localChocos + $localChocos = $localChocos | Where-Object { (!$_.Feed.IsDefault) -and ($_.Feed.Source -notlike "*nuget/nuget.internal*") -and ($_.Feed.Source -notlike "*SRETools*") } + + $splitOption = [System.StringSplitOptions]::RemoveEmptyEntries + $newPackages = @(); + if($packagesToInstallInput) { + Write-Host "`n$loglead Parsing list of packages to be installed." + $splitPackagesToInstall = $packagesToInstallInput.Split([Environment]::NewLine, $splitOption) + $packagesToInstall = Format-ParseChocoPackages $splitPackagesToInstall " " + + if($includeFeeds.IsPresent) { + Set-ChocoPackageSourceFeeds $packagesToInstall + } + + Write-Host "`n$loglead Determining which packages haven't been installed before." + foreach($package in $packagesToInstall) { + $installedAlreadySearch = $localChocos | Where-Object { $_.Name -eq $package.Name } + if(!$installedAlreadySearch) { + $newPackages += $package; + } + } + } + + Write-Host "`n$loglead Finding chocolatey services running as windows services." + $chocoServices = Get-ChocolateyServices -IncludeDisabled + + Write-Host "`n$loglead Determining which chocolatey services have migrations, or not" + $servicesWithoutMigrations = @(); + $servicesWithMigrations = @(); + + $chocoInstallPath = Get-ChocolateyInstallPath + $chocoPath = "$chocoInstallPath\lib\" + + foreach($service in $chocoServices) { + Write-Verbose "Checking if service $($service.Name) has migrations" + + $serviceUser = Get-WindowsServiceUser $service.Name + + $execName = (Get-WindowsServiceApplicationName $service.Name).Replace("`"", "") + + $chocoName = $null + if($execName.length -ge $chocoPath.length) { + $endIndex = $execName.IndexOf('\', $chocoPath.length) + if($endIndex -ge 0) { + $chocoName = $execName.Substring($chocoPath.length, $endIndex - $chocoPath.length) + } + } + + # Determine services with/without migrations. + # Search from the $packagesToInstall first so newer choco versions are preferred. + # Otherwise fall back to the one that's already installed. + $package = $null + if($packagesToInstall) { + $package = $packagesToInstall | Where-Object { $_.Name -eq $chocoName } | Select-Object -First 1 + } + if(!$package) { + $package = $localChocos | Where-Object { $_.Name -eq $chocoName } | Select-Object -First 1 + } + + if($package) { + if($serviceUser -match "dbms\$*$") { + $servicesWithMigrations += $package; + } + # Handle dbms migration overrides. Not using a hash map because hash lookups are case sensitive. + elseif($null -ne ($migrationPackageOverrides | Where-Object {$package.Name -eq $_})) { + Write-Warning "Overriding $($package.Name) to be considered a microservice with migrations." + $servicesWithMigrations += $package; + } elseif($serviceUser -match "micro\$*$") { + $servicesWithoutMigrations += $package; + } else { + Write-Warning "$loglead Package $($package.Name) is not recognized as a micro/dbms Alkami windows service." + } + } else { + Write-Warning "$loglead No recognized windows service for choco package: $($service.Name)" + } + } + + # Now come up with a list of every package that is not in the list of services with/without migrations. + # This will catch the packages that only copy files. + Write-Host "$loglead Now determining which chocolatey packages are not windows services." + $otherPackages = @() + foreach($package in $localChocos) { + $search = $servicesWithMigrations | Where-Object { $_.Name -eq $package.Name } | Select-Object -First 1 + if(!$search) { + $search = $servicesWithoutMigrations | Where-Object { $_.Name -eq $package.Name } | Select-Object -First 1 + } + if(!$search) { + $search = $packagesToInstall | Where-Object { $_.Name -eq $package.Name } | Select-Object -First 1 + if($search) { + $otherPackages += $search + } else { + $otherPackages += $package + } + } + } + + # Separate out the choco installer packages from the 'other' packages. + $installerPackages = $otherPackages | Where-Object { (Test-IsPackageInstaller -packageName $_.Name) } + $otherPackages = $otherPackages | Where-Object { !(Test-IsPackageInstaller -packageName $_.Name) } + + Write-Host "`n$loglead New Packages:" + foreach($package in $newPackages) { + Write-Host " $($package.Name) $($package.Version)" + } + + Write-Host "`n$loglead Other Packages:" + foreach($package in $otherPackages) { + Write-Host " $($package.Name) $($package.Version)" + } + + Write-Host "`n$loglead Services With Migrations:" + foreach($package in $servicesWithMigrations) { + Write-Host " $($package.Name) $($package.Version)" + } + + Write-Host "`n$loglead Services Without Migrations:" + foreach($package in $servicesWithoutMigrations) { + Write-Host " $($package.Name) $($package.Version)" + } + + Write-Host "`n$loglead Microservice Installer Packages:" + foreach($package in $installerPackages) { + Write-Host " $($package.Name) $($package.Version)" + } + + $result = @{ + New = $newPackages + Other = $otherPackages + WithMigrations = $servicesWithMigrations + WithoutMigrations = $servicesWithoutMigrations + Installer = $installerPackages + } + + return $result; +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.ps1 new file mode 100644 index 0000000..e06b90a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.ps1 @@ -0,0 +1,34 @@ +function Get-ChocoPackageFromPath { +<# +.SYNOPSIS + Given a path, determine the package name from the path. + May return null +#> + [CmdletBinding()] + [OutputType([System.String],[System.Void])] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + $logLead = (Get-LogLeadName) + + $chocolateyRootPath = (Get-ChocolateyInstallPath) + + if (!($Path.ToLower().StartsWith($chocolateyRootPath.ToLower()))) { + Write-Warning "$logLead : Path isn't in a chocolatey subfolder" + Write-Verbose "$logLead : Path [$Path] does not start with [$chocolateyRootPath], returning $null" + return $null + } + + $pathRemainder = $Path.Substring($chocolateyRootPath.Length) + + $lastSplitPath = '' + while (($pathRemainder -ne '\lib') -and ($pathRemainder -ne '\lib-bad') -and ($pathRemainder -ne '\lib-bkp') -and ($pathRemainder -ne '\') -and ![string]::IsNullOrWhiteSpace($pathRemainder)) { + $lastSplitPath = (Split-Path $pathRemainder -Leaf) + $pathRemainder = (Split-Path $pathRemainder -Parent) + } + + return $lastSplitPath +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.tests.ps1 new file mode 100644 index 0000000..2017a32 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPackageFromPath.tests.ps1 @@ -0,0 +1,38 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ChocoPackageFromPath" { + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyInstallPath -MockWith { return "C:\ProgramData\chocolatey" } + + $ExpectedPackageId = "Alkami.PowerShell.AD" + + It "is empty if the path is garbage" { + Get-ChocoPackageFromPath -Path "garbage" | Should -BeNullOrEmpty + } + + It "is empty if the path is the choco folder" { + Get-ChocoPackageFromPath -Path "C:\ProgramData\chocolatey" | Should -BeNullOrEmpty + } + + It "is the wrong subfolder if it's not a package installation folder" { + Get-ChocoPackageFromPath -Path "C:\PROGRAMDATA\Chocolatey\nonsense\Alkami.PowerShell.AD\" | Should -Be "nonsense" + } + + It "is the package but is in the backup folder location" { + Get-ChocoPackageFromPath -Path "C:\PROGRAMDATA\Chocolatey\lib-bkp\Alkami.PowerShell.AD\" | Should -Be $ExpectedPackageId + } + + It "is the package that doesn't have a subfolder" { + Get-ChocoPackageFromPath -Path "C:\PROGRAMDATA\Chocolatey\lib\Alkami.PowerShell.AD\" | Should -Be $ExpectedPackageId + } + + It "is is the pacakge with a subfolder (and different case)" { + Get-ChocoPackageFromPath -Path "C:\PROGRAMDATA\Chocolatey\lib\Alkami.PowerShell.AD\tools" | Should -Be $ExpectedPackageId + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPublicPassThruFeedUrl.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPublicPassThruFeedUrl.ps1 new file mode 100644 index 0000000..f48944f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoPublicPassThruFeedUrl.ps1 @@ -0,0 +1,11 @@ +function Get-ChocoPublicPassThruFeedUrl { +<# +.SYNOPSIS + Get the Choco public pass-thru feed URL +#> + [OutputType([string])] + [CmdletBinding()] + Param() + + return "https://packagerepo.orb.alkamitech.com/nuget/chocolatey.org" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.ps1 new file mode 100644 index 0000000..8963ee3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.ps1 @@ -0,0 +1,115 @@ +function Get-ChocoState { + <# +.SYNOPSIS + Returns a list of chocolatey package objects by directly calling choco.exe console application + +.PARAMETER LocalOnly + Limit to listing Locally installed packages ONLY + +.PARAMETER Source + The chocolatey source to query + +.PARAMETER PackageName + The package name to search for + IMPORTANT - By default, chocolatey will search all fields of a package for this string. It is not intrinsically a package "name". + If you specifically want to search for package names, pass the Exact switch. + +.PARAMETER All + Pass the "-a" flag to "choco.exe list" + +.PARAMETER Pre + Include pre-release packages in results + +.PARAMETER GetServiceInfo + Include partial Win32_Service fields in each result + +.PARAMETER Exact + ONLY search package NAMES for the string passed as "PackageName" +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Alkami generates this string manually, no user injection')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("local", "l", "lo")] + [switch]$LocalOnly, + + [Parameter(Mandatory = $false)] + [Alias("s")] + [string]$Source = "", + + [Parameter(Mandatory = $false)] + [string]$PackageName = "", + + [Parameter(Mandatory = $false)] + [string]$PackageVersion = "", + + [Parameter(Mandatory = $false)] + [Alias("a")] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$Pre, + + [Parameter(Mandatory = $false)] + [switch]$GetServiceInfo, + + [Parameter(Mandatory = $false)] + [switch]$Exact + ) + $logLead = (Get-logLeadName) + + + $chocoPackages = @(); + + $localFlag = if ($LocalOnly.IsPresent) { "-l" } else { "" } + $sourceArg = if (![string]::IsNullOrWhitespace($Source)) { "-s ""$Source""" } else { "" } + + if (Test-StringIsNullOrWhitespace -Value $PackageName) { + $PackageName = "" + } + + if (Test-StringIsNullOrWhitespace -Value $PackageVersion) { + $packageVersionString = "" + } else { + $packageVersionString = "--version $PackageVersion" + } + + $allFlag = ""; + if ($All.IsPresent) { + $allFlag = "-a -pre" + } elseif ($Pre.IsPresent) { + $allFlag = "-pre" + } + + $exactFlag = if ($Exact.IsPresent) { "-e" } else { "" } + + $command = "choco list $PackageName $packageVersionString -r $localFlag $sourceArg $allFlag $exactFlag" + $iexCommand = {Invoke-Expression -Command $command} + Write-Host "$logLead : Command: $command" + + $chocoList = Invoke-CommandWithRetry -ScriptBLock $iexCommand -Exponential + $chocoList = $chocoList | Where-Object { (![string]::IsNullOrWhitespace($_)) -and ($_ -notmatch "Chocolatey v\d") } + + # There are multiple possible connection error messages, that come back across multiple lines + # If another message for connection errors is found, add it to the "regexes" + if ($chocoList -match "Unable to connect|Error retrieving packages from source") { + Write-Error -Message "$logLead : Unable to connect to Package Server" + } + $filteredList = $chocoList.Where( { $_ -notmatch "Unable to connect|Error retrieving packages from source" }) + $chocoPackages = (Format-ParseChocoPackages $filteredList) + + if ($GetServiceInfo) { + $installedServices = Get-CimInstance Win32_Service + + foreach ($package in $chocoPackages) { + foreach ($service in $installedServices) { + if ($package.Name -eq $service.Name) { + $package.IsService = $true + $package.StartMode = $service.StartMode + } + } + } + } + + return $chocoPackages +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.tests.ps1 new file mode 100644 index 0000000..eba204e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocoState.tests.ps1 @@ -0,0 +1,71 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$fake_Output_OnlyConnectionErrors = @( + "[NuGet] Not able to contact source 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/'. Error was Unable to connect to the remote server", + "[NuGet] Not able to contact source 'https://packagerepo.orb.alkamitech.com/nuget/SRETools'. Error was Unable to connect to the remote server", + "Error retrieving packages from source 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/':", + " Unable to connect to the remote server", + "Error retrieving packages from source 'https://packagerepo.orb.alkamitech.com/nuget/SRETools':", + " Unable to connect to the remote server" +) +$fake_Output_Packages_Plus_ConnectionErrors = @( + "[NuGet] Not able to contact source 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/'. Error was Unable to connect to the remote server", + "[NuGet] Not able to contact source 'https://packagerepo.orb.alkamitech.com/nuget/SRETools'. Error was Unable to connect to the remote server", + "Error retrieving packages from source 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/':", + " Unable to connect to the remote server", + "Error retrieving packages from source 'https://packagerepo.orb.alkamitech.com/nuget/SRETools':", + " Unable to connect to the remote server", + "test.package.name|1.0.0" +) +$fake_NormalOutput = @("test.package.name|1.0.0","test.otherpackage.name|1.0.0") + +Describe "Get-ChocoState" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return ""} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Invoke-Expression -MockWith {} + #Mock -ModuleName $moduleForMock -CommandName Format-ParseChocoPackages -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-CimInstance -MockWith {} + + Context "Connection_Error_Handling" { + + It "WithOnlyConnectionErrors_Returns_NullOrEmpty" { + Mock -ModuleName $moduleForMock -CommandName Invoke-Expression -MockWith { + return $fake_Output_OnlyConnectionErrors + } + Get-ChocoState | Should -HaveCount 0 + Assert-MockCalled -CommandName Write-Error -ModuleName $moduleForMock -ParameterFilter { + $Message -match "Unable to connect" + } -Times 1 -Scope It + } + It "WithBothPackagesAndConnectionErrors_Writes_Error" { + # This one is weird because of mocking and ErrorAction and Pester + # There may or may not be results, but there should always be a Write-Error + # This needs more consideration - TR + Mock -ModuleName $moduleForMock -CommandName Invoke-Expression -MockWith { + return $fake_Output_Packages_Plus_ConnectionErrors + } + Get-ChocoState #| Should -HaveCount 0 + Assert-MockCalled -CommandName Write-Error -ModuleName $moduleForMock -ParameterFilter { + $Message -match "Unable to connect" + } -Times 1 -Scope It + } + It "NoConnectionErrors_Returns_List" { + Mock -ModuleName $moduleForMock -CommandName Invoke-Expression -MockWith { + return $fake_NormalOutput + } + Get-ChocoState | Should -HaveCount 2 + Assert-MockCalled -CommandName Write-Error -ModuleName $moduleForMock -ParameterFilter { + $Message -match "Unable to connect" + } -Times 0 -Scope It + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.ps1 new file mode 100644 index 0000000..8647365 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.ps1 @@ -0,0 +1,58 @@ +function Get-ChocolateyParameterString { +<# +.SYNOPSIS + Get the complex parameter string that gets passed on package installation to all installable packages. + +.DESCRIPTION + Get the complex parameter string that gets passed on package installation to all installable packages. + We pass these values at all times even if the installing package doesn't know about the parameters for consistency. + +.PARAMETER Package + [PSObject] Must have a property Name that indicates the package being installed + +.PARAMETER Environment + Optional. The environment this package is being installed to. + +.PARAMETER RunMigrations + [Switch] If this package has migraitons, should migrations be run for this package at install? + +.PARAMETER Package + [Switch] If this is a service, should it be started? +#> + [OutputType([string])] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [object]$Package, + + [Parameter(Mandatory = $false)] + [Alias("env","e")] + [string]$Environment, + + [Parameter(Mandatory = $false)] + [Alias("migrate","m")] + [bool]$RunMigrations = $true, + + [Parameter(Mandatory = $false)] + [Alias("start","s")] + [bool]$StartService = $false + ) + + # Determine optional New Relic app name based on the package + $newRelicParam = ""; + if ($Environment) + { + $newRelicName = (Get-NewRelicAppNameForConfigurationValue $Package.Name) + $newRelicParam = "/NewRelicAppName:'$Environment $newRelicName'"; + } + + $boolString = if($RunMigrations) { "true" } else { "false" } + $migrationParam = "/MigrationsEnabled:$boolString" + + $boolString = if($StartService) { "true" } else { "false" } + $startupParam = "/Alkami.Installer.ServiceStartupMode:$boolString" + + $chocoParamString = "`"$migrationParam $startupParam $newRelicParam`""; + return $chocoParamString; +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.tests.ps1 new file mode 100644 index 0000000..662600b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateyParameterString.tests.ps1 @@ -0,0 +1,82 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ChocolateyParameterString" { + $testCases = @() + foreach($testCase in @( + @{Name = 'Alkami.Api.OrbFX'; Short = 'ORBFX';} + @{Name = 'Alkami.Api.CUFX'; Short = 'CUFX';} + @{Name = 'Alkami.Security.RPSTS'; Short = 'RP-STS';} + @{Name = "Alkami.App.Providers.SymConnectMultiplexer"; Short = "SymConnect";} + @{Name = 'unknown package name'; Short = 'unknown package name';} + )) { + $testCases += @{ + caseDescription = 'Matches the expected response string when only package is specified' + package = @{ Name = $testCase.Name } + testName = $testCase.Name + environment = '' + runMigrations = $false + startService = $false + outputExpected = '"/MigrationsEnabled:false /Alkami.Installer.ServiceStartupMode:false "' + } + + $testCases += @{ + caseDescription = 'Matches the expected response string when environment is not empty' + package = @{ Name = $testCase.Name } + testName = $testCase.Name + environment = 'Not A Real Environment' + runMigrations = $false + startService = $false + outputExpected = ('"/MigrationsEnabled:false /Alkami.Installer.ServiceStartupMode:false /NewRelicAppName:''Not A Real Environment {0}''"' -f $testCase.Short) + } + + $testCases += @{ + caseDescription = 'Matches the expected response string when runMigrations is true' + package = @{ Name = $testCase.Name } + testName = $testCase.Name + environment = '' + runMigrations = $true + startService = $false + outputExpected = '"/MigrationsEnabled:true /Alkami.Installer.ServiceStartupMode:false "' + } + + $testCases += @{ + caseDescription = 'Matches the expected response string when startServices is true' + package = @{ Name = $testCase.Name } + testName = $testCase.Name + environment = '' + runMigrations = $false + startService = $true + outputExpected = '"/MigrationsEnabled:false /Alkami.Installer.ServiceStartupMode:true "' + } + + $testCases += @{ + caseDescription = 'Matches the expected response string when environment is not empty and runMigrations is true and startServices is true' + package = @{ Name = $testCase.Name } + testName = $testCase.Name + environment = 'Not A Real Environment' + runMigrations = $true + startService = $true + outputExpected = ('"/MigrationsEnabled:true /Alkami.Installer.ServiceStartupMode:true /NewRelicAppName:''Not A Real Environment {0}''"' -f $testCase.Short) + } + } + + Context "Test basic scenarios" { + It " for " -TestCases $testCases { + param( + $package, + $environment, + $runMigrations, + $startService, + $outputExpected + ) + + (Get-ChocolateyParameterString -package $package -environment $environment -runMigrations $runMigrations -startService $startService) | Should -Be $outputExpected + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySources.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySources.ps1 new file mode 100644 index 0000000..dd4a310 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySources.ps1 @@ -0,0 +1,59 @@ +function Get-ChocolateySources { +<# +.SYNOPSIS + Returns a list of locally configured chocolatey feed objects. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + + [Parameter(Mandatory = $false)] + [Alias("IncludeDisabled")] + [switch]$includeDisabledSources + ) + $logLead = (Get-logLeadName) + + # Grab chocolatey feeds configured on the local machine or server. + $sources = $null; + if(($hostname -eq "localhost") -or ($hostname -eq ".")) + { + $sources = (choco source list -r); + } + else + { + $sources = Invoke-Command -ComputerName $hostname -ScriptBlock { return (choco source list -r) }; + } + + $alkamiFeedMatch = "https://packagerepo.orb.alkamitech.com/nuget/choco.*"; + $sdkFeedMatch = "https://feeds.alkamitech.com/nuget/*"; + + $resultSources = @(); + + foreach ($sourceToCheck in $sources) { + $splitSource = $sourceToCheck.Split("|") + + if (!($includeDisabledSources) -and ($splitSource[2] -eq "True")) { + Write-Host "$logLead : Skipping $sourceToCheck" + continue; + } + + $isDefaultFeed = $splitSource[0] -eq "chocolatey"; + $properties = @{ + Name = $splitSource[0]; + Source = $splitSource[1]; + Priority = $splitSource[5]; + Disabled = if ($splitSource[2] -like "true") { $true; } else { $false; } + IsDefault = $isDefaultFeed; + IsSDK = (!$isDefaultFeed) -and ($splitSource[1] -like $sdkFeedMatch); + }; + $source = New-Object -TypeName PSObject -Prop $properties; + + $resultSources += $source; + } + + # Re-arrange the choco sources to prioritize the Alkami choco.* feeds to the top, and SDK feeds second. It speeds up searching! + $resultSources = ($resultSources | Sort-Object -Property @{Expression={ [int]($_.Source -like $alkamiFeedMatch) * 2 + [int]($_.Source -like $sdkFeedMatch)}} -Descending); + + return $resultSources; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySourcesV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySourcesV2.ps1 new file mode 100644 index 0000000..6060615 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ChocolateySourcesV2.ps1 @@ -0,0 +1,63 @@ +function Get-ChocolateySourcesV2 { +<# +.SYNOPSIS + Returns a list of locally configured chocolatey feed objects. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + + [Parameter(Mandatory = $false)] + [Alias("IncludeDisabled")] + [switch]$includeDisabledSources + ) + + $logLead = (Get-LogLeadName) + + $sb = { + [xml](Get-Content "C:\ProgramData\chocolatey\config\chocolatey.config") + } + + $sources = @() + + if (Compare-StringToLocalMachineIdentifiers -stringToCheck $hostname) { + $sources = (Invoke-Command -ScriptBlock $sb).chocolatey.sources + } else { + $sources = (Invoke-Command -ComputerName $hostname -ScriptBlock $sb).chocolatey.sources + } + $alkamiHostname = 'packagerepo.orb.alkamitech.com' + $sdkHostname = 'feeds.alkamitech.com' + $defaultFeed = 'chocolatey.org' + + $resultSources = @() + foreach ($source in $sources.source) { + $uri = [System.Uri]::new($source.value) + $isSdkFeed = $uri.Host -eq $sdkHostname + $isAlkamiFeed = $uri.Host -eq $alkamiHostname + $isDefault = $source.value -match $defaultFeed + $isSre = ($source.value -match 'SRE') -or ($source.value -match 'nuget.internal') + $priority = [int]$source.Priority + + $properties = @{ + Name = $source.Id + Source = $source.Value + Priority = $priority + Disabled = [bool]::Parse($source.Disabled) + IsDefault = $isDefault + IsSDK = $isSdkFeed + Ordering = ($priority + (2 * [int]$isAlkamiFeed) + [int]$isSdkFeed - [int]$isSre - (2 * [int]$isDefault)) + } + $source = New-Object -TypeName PSObject -Prop $properties + + if (!$source.Disabled -or ($includeDisabledSources -and $source.Disabled)) { + $resultSources += $source + } else { + Write-Host "$logLead : Skipping return of [$($source.Name)] with url [$($source.Source)] due to it is disabled, and no flag is provided to include disabled feeds" + } + } + + $resultSources = $resultSources | Sort-Object -Property Ordering -Descending + + return $resultSources +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-FriendlyChocoPackageName.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-FriendlyChocoPackageName.ps1 new file mode 100644 index 0000000..670c90c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-FriendlyChocoPackageName.ps1 @@ -0,0 +1,33 @@ +function Get-FriendlyChocoPackageName { +<# +.SYNOPSIS + Returns an Alkami package name stripped of redundant naming patterns, for ease of readability. +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$packageName + ) + + $nameCleanupPatterns = @( + "Alkami\.Apps\.", + "Alkami\.App\.Providers.", + "Alkami\.App\.", + "Alkami\.Api\.", + "Alkami\.Client\.", + "Alkami\.Admin\.", + "Alkami\.MicroServices\.", + "Alkami\.Modules\.", + "Alkami\.WebExtensions\.", + "Alkami\.", + "\.Service\.Host" + "\.Host" + ) + + foreach($pattern in $nameCleanupPatterns) + { + $packageName = $packageName -replace $pattern,""; + } + return $packageName; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-LocallyInstalledChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-LocallyInstalledChocoPackages.ps1 new file mode 100644 index 0000000..8c9e806 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-LocallyInstalledChocoPackages.ps1 @@ -0,0 +1,20 @@ +function Get-LocallyInstalledChocoPackages { +<# +.SYNOPSIS + Returns the currently installed chocolatey packages + +.PARAMETER LimitOutput + Used for -r flag functionality. Returns a machine parseable list of chocolatey records +#> + [CmdletBinding()] + param ( + [switch]$LimitOutput + ) + + $limitOutputFlag = "" + if($LimitOutput) { + $limitOutputFlag = "-r" + } + + return (choco list -l $limitOutputFlag) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-MicroserviceTiers.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-MicroserviceTiers.ps1 new file mode 100644 index 0000000..3c1a322 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-MicroserviceTiers.ps1 @@ -0,0 +1,10 @@ +function Get-MicroserviceTiers { +<# +.SYNOPSIS +Returns a list of arrays organizing package names into dependent tiers of installation. +#> + [CmdletBinding()] + Param() + + return $_MicroserviceTiers; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-MicroservicesWithMigrations.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-MicroservicesWithMigrations.ps1 new file mode 100644 index 0000000..b43a9c0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-MicroservicesWithMigrations.ps1 @@ -0,0 +1,66 @@ +function Get-MicroservicesWithMigrations { +<# +.SYNOPSIS + Finds microservices which have migrations. Can be used during a lane or POD move to identify Microservices which need to be reinstalled + +.NOTES + For now, we assume any service running as DBMS Potentially Has Migrations +#> + ## TODO: Cole refactor with manifest to determine if there's a migration + [CmdletBinding()] + [OutputType([System.Object])] + Param() + + $logLead = Get-LogLeadName + Write-Verbose "$logLead : Looking for Services Running as DBMS to Reinstall" + + # Get all Chocolatey Services + $chocoServices = Get-ChocolateyServices + Write-Verbose ("$logLead : Found {0} Chocolatey Services" -f $chocoServices.Count) + + $chocoServicesToReinstall = @() + + Write-Verbose "$logLead : Getting Formatted Local Packages" + $localChocos = Get-ChocoState -l + Write-Verbose ("$logLead : Found {0} Local Packages" -f $localChocos.Count) + + $chocoServices | ForEach-Object { + + $svc = $_ + $serviceUser = Get-WindowsServiceUser $svc.Name + + # If the service runs as DBMS and is not disabled, continue + if ($svc.StartType -eq [System.ServiceProcess.ServiceStartMode]::Disabled) { + Write-Verbose ("$logLead : Skipping Service {0} as it is Disabled" -f $svc.Name) + } elseif ($serviceUser -notmatch "dbms\$*$") { + Write-Verbose ("$logLead : Skipping Service {0} as it is Running as {1}" -f $svc.Name, $serviceUser) + } else { + Write-Verbose ("$logLead : Getting information on services {0} for reinstall" -f $svc.Name) + + $localBinPath = Get-WindowsServiceApplicationPath $svc.Name + Write-Verbose ("$logLead : Read Installation Path as {0}" -f $localBinPath) + + if ([string]::IsNullOrEmpty($localBinPath)) { + Write-Warning ("$logLead : Could not read install path for service: " + $svc.Name) + } else { + $installPath = (Get-Item $localBinPath).Parent + } + + if ([String]::IsNullOrEmpty($installPath)) { + Write-Warning ("$logLead : Could not read install path for service: " + $svc.Name) + } else { + Write-Verbose ("$logLead : Looking for Packages Matching Name {0}" -f $installPath.Name) + [array]$choco = ($localChocos | Where-Object { $_.Name -match $installPath.Name }) + if ($null -eq $choco -or $choco.Count -ne 1) { + Write-Warning ("$logLead : Found {0} Packages Matching Name {1}" -f (IsNull $choco.Count 0), $installPath.Name) + } else { + Write-Host ("$logLead : Adding Package {0} with version {1}" -f $choco.Name, $choco.Version) + $chocoServicesToReinstall += @{Name = $choco.Name; Version = $choco.Version; } + } + } + } + } + + return $chocoServicesToReinstall +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAlkamiManifestV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAlkamiManifestV2.ps1 new file mode 100644 index 0000000..520bc58 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAlkamiManifestV2.ps1 @@ -0,0 +1,69 @@ +function Get-PackageAlkamiManifestV2 { + <# + .SYNOPSIS + Returns the AlkamiManifest content for the given package if it's found in Proget + + .PARAMETER FeedSource + [string] Source feed used to look up the package by + + .PARAMETER Name + [string] Package name to lookup + + .PARAMETER Version + [string] Package version to lookup + + .PARAMETER Package + [Object] Known package object with properties as { Feed={ Source=; Name=; } Name=; Version=; } + + .PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed + #> + [CmdletBinding(DefaultParameterSetName='RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + ## TODO: Can this pull from the local filesystem if it exists? + ## This would let us fetch faster if the versions match as we could avoid network hops. + + $loglead = Get-LogLeadName + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $FeedSource = $Package.Feed.Source + $Name = $Package.Name + $Version = $Package.Version + } + + Write-Verbose "$loglead : Querying for Alkami manifest from proget for package [$Name]" + + $splatVar = @{ + FeedSource = $FeedSource + Name = $Name + Version = $Version + Credential = $Credential + + PackagePath = "" #filled in below + } + + foreach($validManifestFilename in (Get-ValidPackageManifestFilenames)) { + $splatVar.PackagePath = $validManifestFilename + $result = (Get-PackageFileV2 @splatVar) + if (![string]::IsNullOrWhiteSpace($result)) { + return (Get-PackageManifest -RawContent $result -PackageName $Name -ManifestSource "$FeedSource $Name $Version") + } + } + + Write-Host "$logLead : No manifest found in Proget for [$Name]" + return $null + } diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAppConfigXml.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAppConfigXml.ps1 new file mode 100644 index 0000000..a47153c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageAppConfigXml.ps1 @@ -0,0 +1,25 @@ +function Get-PackageAppConfigXml { +<# +.SYNOPSIS + Returns the App.Config XML object for the given package. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$FeedSource, + [Parameter(Mandatory=$true)] + [string]$Name, + [Parameter(Mandatory=$true)] + [string]$Version, + [Parameter(Mandatory=$false)] + [PSCredential]$Credential = $null + ) + $loglead = Get-LogLeadName + + # Query for the nuspec from Proget. + Write-Verbose "$loglead : Querying for App.Config for package $Name $Version"; + $packagePath = "tools/$($Package.Name).exe.config" + [xml]$result = (Get-PackageFile -feedSource $FeedSource -name $Name -version $Version -packagePath $packagePath -Credential $Credential); + + return $result; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFile.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFile.ps1 new file mode 100644 index 0000000..b40858e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFile.ps1 @@ -0,0 +1,78 @@ +function Get-PackageFile { + <# +.SYNOPSIS + Downloads a specific file from proget and returns it. Throws an exception if the file doesn't exist. + This should only be used to download string/config files. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$FeedSource, + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Version, + [Parameter(Mandatory = $true)] + [string]$PackagePath, + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName); + + + # Make sure the input feed URL is a nuget feed. + $url = $feedSource.Replace("\", "/") + if ($url -notlike "https://*/nuget/*") { + Write-Error "$loglead : Feed URL must be in the format of `"https://feed.com/nuget/feed.name`"" + return + } + + # If a PSCredential is specified build a basic authentication header. + $headers = (Get-BasicAuthHeader -Credential $Credential) + + # Replace package path back slashes with forward slashes, to play nicely in the URL. + $packagePath = $packagePath.Replace("\", "/") + + # Parse out the base proget URL and the feed name. + $searchString = "/nuget/" + $index = $url.IndexOf($searchString) + $baseUrl = $url.Substring(0, $index) + $feedName = $url.Substring($index + $searchString.Length) + $feedName = $feedName.TrimEnd('/') + Write-Verbose "$loglead : Base URL: $baseUrl" + Write-Verbose "$loglead : Feed Name: $feedname" + + # Make sure the case sensitivity of the package name is correct by querying for package files, and + # looking for the specific file in a case insensitive manner. + $filesUrl = "$baseUrl/package-files/list?packageId=$($name)&version=$($version)&feedName=$($feedName)" + Write-Host "$loglead : Querying for package files at endpoint: $filesUrl" + try { + $filesResponse = Invoke-ProgetRequest -URI $filesUrl -Headers $headers + $files = [System.IO.StreamReader]::new($filesResponse.RawContentStream).ReadToEnd() | ConvertFrom-Json + $fileSearch = $files | Where-Object { $_.fullPath -like $packagePath } + } catch { + $fileSearch = $null + } + Write-Host "$loglead : Done Querying for package files at endpoint: $filesUrl" + + if ($null -eq $fileSearch) { + Write-Warning "Package file `"$($packagePath)`" could not be found in $name $version" + return $null + } + $packagePath = $fileSearch | Select-Object -First 1 -ExpandProperty "fullPath" + + # Note: This is a query to download the package .nuspec file. This is a not a query for the filenames/contents of the package. + $nuspecUrl = "$baseUrl/package-files/download?packageId=$name&version=$version&feedName=$feedName&path=$packagePath" + + # Download. + Write-Host "$loglead : Querying for nuspec file at endpoint: $nuspecUrl" + $response = Invoke-ProgetRequest -URI $nuspecUrl -Headers $headers + + + # StreamReader interprets byte order marks and skips it, if it exists. + $result = [System.IO.StreamReader]::new($response.RawContentStream).ReadToEnd() + Write-Host "$loglead : Completed querying nuspec file at endpoint: $nuspecUrl" + + return $result +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileList.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileList.ps1 new file mode 100644 index 0000000..f9ad062 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileList.ps1 @@ -0,0 +1,49 @@ +function Get-PackageFileList { +<# +.SYNOPSIS + Returns the json object of a package's file contents. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$FeedSource, + [Parameter(Mandatory=$true)] + [string]$Name, + [Parameter(Mandatory=$true)] + [string]$Version, + [Parameter(Mandatory=$false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName) + + + # Make sure the input feed URL is a nuget feed. + $url = $feedSource.Replace("\", "/") + if($url -notlike "https://*/nuget/*") { + Write-Error "$loglead : Feed URL must be in the format of `"https://feed.com/nuget/feed.name`"" + return + } + + # If a PSCredential is specified build a basic authentication header. + $headers = (Get-BasicAuthHeader -Credential $Credential) + + # Parse out the base proget URL and the feed name. + $searchString = "/nuget/" + $index = $url.IndexOf($searchString) + $baseUrl = $url.Substring(0,$index) + $feedName = $url.Substring($index + $searchString.Length) + $feedName = $feedName.TrimEnd('/') + Write-Verbose "$loglead : Base URL: $baseUrl" + Write-Verbose "$loglead : Feed Name: $feedname" + + # Query for all the files in the package. + # Note: This is a query for the filenames/contents of the package, and not a query to download a particular file. + $filesUrl = "$baseUrl/package-files/list?packageId=$($name)&version=$($version)&feedName=$($feedName)" + Write-Host "$loglead : Querying for package files at endpoint: $filesUrl" + + $response = Invoke-ProgetRequest -URI $filesUrl -Headers $headers + $return = (ConvertFrom-Json -InputObject $response) + Write-Host "$loglead : Completed querying package files at endpoint: $filesUrl" + return $return +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.ps1 new file mode 100644 index 0000000..d2373fb --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.ps1 @@ -0,0 +1,92 @@ +function Get-PackageFileListV2 { + <# +.SYNOPSIS + Downloads a specific file from proget and returns it. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER PackagePath + Ignored. Provided for contract simplicity + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName = 'RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory = $false)] + [string]$PackagePath, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + ## TODO: Can this pull from the local filesystem if it exists? + ## This would let us fetch faster if the versions match as we could avoid network hops. + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $FeedSource = $Package.Feed.Source + $Name = $Package.Name + $Version = $Package.Version + } + + # Replace package path back slashes with forward slashes, to play nicely in the URL. + $packagePath = $packagePath.Replace("\", "/") + + # Make sure the input feed URL is a nuget feed. + $feedUri = $null + try { + $feedUri = [System.Uri]::new($FeedSource) + if ($feedUri.Segments.Count -lt 3) { + throw "$logLead : FeedSource [$FeedSource] parameter for package [$Name] should have at least three url segments (host, nuget, feed name) and does not have enough feed segments provided." + } + $segmentCompare = 'nuget/' + if ($feedUri.Segments[1] -ne $segmentCompare) { + throw "$logLead : FeedSource [$FeedSource] parameter for package [$Name] should use the url segment [$segmentCompare], not [$($feedUri.Segments[1])]." + } + } catch { + Write-Error "$logLead : FeedSource [$FeedSource] parameter for package [$Name] was supplied incorrectly. Expected url pattern should look like: [https://feed.com/nuget/feed.name]" + Write-Error $_.Exception.Message + return + } + + # we now have a valid feedUri, it has the right segment count, and we can infer otherwise as needed + $feedName = $feedUri.Segments[2].TrimEnd('/') + $baseUrl = $feedUri.GetComponents([System.UriComponents]::SchemeAndServer, [System.UriFormat]::SafeUnescaped) + $filesUrl = "$baseUrl/package-files/list?packageId=$Name&version=$Version&feedName=$feedName" + # If a PSCredential is specified build a basic authentication header. + $headers = (Get-BasicAuthHeader -Credential $Credential) + + # Query for all the files in the package. + Write-Host "$loglead : Querying for package files at endpoint: $filesUrl" + $response = Invoke-ProgetRequest -URI $filesUrl -Headers $headers + Write-Host "$loglead : Done Querying for package files at endpoint: $filesUrl" + + try { + $jsonResponse = (ConvertFrom-Json -InputObject $response) + } catch { + Write-Error "Did not recieve json from proget request" + } + + # Consider this a best practice, per gwhiting + $inlineFilterVariable = $jsonResponse.Where({!$_.fullPath.StartsWith("src/")}) + return $inlineFilterVariable +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.tests.ps1 new file mode 100644 index 0000000..a5117af --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileListV2.tests.ps1 @@ -0,0 +1,140 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Get-PackageFileListV2" { + # subset of actual failing case + # Test case should not show any src/ rooted objects, so we are only focusing on non-SDK source code work + $script:testString = @" +[ + { + "fullPath": "src/VCU.MS.CardControl.Notifications/", + "parentFullPath": "src/", + "name": "VCU.MS.CardControl.Notifications", + "isDirectory": true + }, + { + "fullPath": "src/", + "parentFullPath": "/", + "name": "src", + "isDirectory": true + }, + { + "fullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/", + "parentFullPath": "src/VCU.MS.CardControl.Notifications/", + "name": "VCU.Modules.CardControl", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/core-properties/", + "parentFullPath": "package/services/metadata/", + "name": "core-properties", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/", + "parentFullPath": "package/services/", + "name": "metadata", + "isDirectory": true + }, + { + "fullPath": "package/services/", + "parentFullPath": "package/", + "name": "services", + "isDirectory": true + }, + { + "fullPath": "package/", + "parentFullPath": "/", + "name": "package", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/core-properties/5bb485eb5b744639ac99ff7821cf325a.psmdcp", + "parentFullPath": "package/services/metadata/core-properties/", + "name": "5bb485eb5b744639ac99ff7821cf325a.psmdcp", + "isDirectory": false + }, + { + "fullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/AlkamiManifest.xml", + "parentFullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/", + "name": "AlkamiManifest.xml", + "isDirectory": false + }, + { + "fullPath": "_rels/", + "parentFullPath": "/", + "name": "_rels", + "isDirectory": true + }, + { + "fullPath": "_rels/.rels", + "parentFullPath": "_rels/", + "name": ".rels", + "isDirectory": false + }, + { + "fullPath": "tools/", + "parentFullPath": "/", + "name": "tools", + "isDirectory": true + }, + { + "fullPath": "tools/VCU.MS.CardControl.Notifications.Host.exe", + "parentFullPath": "tools/", + "name": "VCU.MS.CardControl.Notifications.Host.exe", + "isDirectory": false + }, + { + "fullPath": "tools/ChocolateyUninstall.ps1", + "parentFullPath": "tools/", + "name": "ChocolateyUninstall.ps1", + "isDirectory": false + }, + { + "fullPath": "tools/ChocolateyInstall.ps1", + "parentFullPath": "tools/", + "name": "ChocolateyInstall.ps1", + "isDirectory": false + }, + { + "fullPath": "VCU.MS.CardControl.Notifications.Host.nuspec", + "parentFullPath": "/", + "name": "VCU.MS.CardControl.Notifications.Host.nuspec", + "isDirectory": false + }, + { + "fullPath": "[Content_Types].xml", + "parentFullPath": "/", + "name": "[Content_Types].xml", + "isDirectory": false + } +] +"@ + Mock -Module $moduleForMock -CommandName Invoke-ProgetRequest -MockWith { return $script:testString } + Mock -Module $moduleForMock -CommandName Get-BasicAuthHeader -MockWith { return @{} } # just give an empty object for parameter + Mock -Module $moduleForMock -CommandName Write-Host -MockWith { } + + Context "It does not throw when called with valid parameters" { + It "Does not throw" { + {Get-PackageFileListV2 -FeedSource "https://magic.feed/nuget/magic.feed" -Name "ignored" -Version "ignored" } | Should -Not -Throw + } + } + + Context "No /src records returned" { + $FileList = (Get-PackageFileListV2 -FeedSource "https://magic.feed/nuget/magic.feed" -Name "ignored" -Version "ignored") + + It "No record exists with name = AlkamiManifest.xml" { + $FileList.Where({$_.name -eq 'AlkamiManifest.xml'}) | Should -BeNull + } + + It "Definitely has a source record with name = AlkamiManifest.xml tho" { + (ConvertFrom-Json $script:testString).Where({$_.name -eq 'AlkamiManifest.xml'}) | Should -Not -BeNull + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileV2.ps1 new file mode 100644 index 0000000..7c7a274 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageFileV2.ps1 @@ -0,0 +1,90 @@ +function Get-PackageFileV2 { + <# +.SYNOPSIS + Downloads a specific file from proget and returns it. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER PackagePath + [string] Path in the proget folder for the file to retrieve the contents from + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName = 'RawArgs')] + [OutputType([string])] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory = $true)] + [string]$PackagePath, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + ## TODO: Can this pull from the local filesystem if it exists? + ## This would let us fetch faster if the versions match as we could avoid network hops. + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $FeedSource = $Package.Feed.Source + $Name = $Package.Name + $Version = $Package.Version + } + + # Replace package path back slashes with forward slashes, to play nicely in the URL. + $packagePath = $packagePath.Replace("\", "/") + + # Make sure the input feed URL is a nuget feed. + $feedUri = $null + try { + $feedUri = [System.Uri]::new($FeedSource) + if ($feedUri.Segments.Count -lt 3) { + throw "$logLead : FeedSource [$FeedSource] parameter for package [$Name] should have at least three url segments (host, nuget, feed name) and does not have enough feed segments provided." + } + $segmentCompare = 'nuget/' + if ($feedUri.Segments[1] -ne $segmentCompare) { + throw "$logLead : FeedSource [$FeedSource] parameter for package [$Name] should use the url segment [$segmentCompare], not [$($feedUri.Segments[1])]." + } + } catch { + Write-Error "$logLead : FeedSource [$FeedSource] parameter for package [$Name] was supplied incorrectly. Expected url pattern should look like: [https://feed.com/nuget/feed.name]" + Write-Error $_.Exception.Message + return + } + + # we now have a valid feedUri, it has the right segment count, and we can infer otherwise as needed + $feedName = $feedUri.Segments[2].TrimEnd('/') + $baseUrl = $feedUri.GetComponents([System.UriComponents]::SchemeAndServer, [System.UriFormat]::SafeUnescaped) + $downloadUrl = "$baseUrl/package-files/download?packageId=$Name&version=$Version&feedName=$feedName&path=$packagePath" + # If a PSCredential is specified build a basic authentication header. + $headers = (Get-BasicAuthHeader -Credential $Credential) + try { + Write-Host "$loglead : Querying for package files at endpoint: $downloadUrl" + $response = Invoke-ProgetRequest -URI $downloadUrl -Headers $headers + # StreamReader interprets byte order marks and skips it, if it exists. + $result = [System.IO.StreamReader]::new($response.RawContentStream).ReadToEnd() + Write-Host "$loglead : Done Querying for package files at endpoint: $downloadUrl" + + return $result + } catch { + Write-Host $_.Exception.Message + return $null + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.Tests.ps1 new file mode 100644 index 0000000..e811835 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.Tests.ps1 @@ -0,0 +1,293 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +# These are (currently) all integration tests due to various unmocked calls +# It is also weird (in a testing context) to use the same function to mock data that is used to test that data +# It creates a greater sense of safety than is actual + +# Use this in the mocks to ensure we're getting the top level context. +$global:globalRoot = $PSScriptRoot + +Describe "Get-PackageInstallationData-V2" { + + # Global Mocks + Mock Set-ChocoPackageSourceFeeds -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + Mock -CommandName Write-Host -ModuleName "Alkami.PowerShell.Choco" -MockWith {} + Mock -CommandName Write-Warning -ModuleName "Alkami.PowerShell.Choco" -MockWith {} + +#region this seems broken because of invoke-parallel + # TODO: @Tom - Does this even get executed since the only use-case is inside the Invoke-Parallel script? I don't think so. - Cole + $TestIsPackageMicroserviceSB = { + param($NuspecXmlObject,$Package) + return ($Package.Name -like "*.MicroServices.*") -or ($Package.Name -like "*.MS.*") -or ($Package.Name -like "*.Services.*") + } + Mock Test-IsPackageMicroserviceV2 -ModuleName "Alkami.PowerShell.Choco" -MockWith $TestIsPackageMicroserviceSB + Mock Test-IsPackageMicroserviceV2 -ModuleName $moduleForMock -MockWith $TestIsPackageMicroserviceSB +#endregion this seems broken because of invoke-parallel + + Mock Test-IsPackageInFeed -ModuleName $moduleForMock -MockWith {return $true} + Mock Test-PackageHasAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { return $false } + + # this long magic string is a barebones package folder with no useful contents. Just the basic nuget structure and a choco install file set + Mock Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { return (ConvertFrom-Json '[{"fullPath":"package/services/metadata/core-properties/","parentFullPath":"package/services/metadata/","name":"core-properties","isDirectory":true},{"fullPath":"package/services/metadata/","parentFullPath":"package/services/","name":"metadata","isDirectory":true},{"fullPath":"package/services/","parentFullPath":"package/","name":"services","isDirectory":true},{"fullPath":"package/","parentFullPath":"/","name":"package","isDirectory":true},{"fullPath":"package/services/metadata/core-properties/629d5521ad934906a0d3c36051d35320.psmdcp","parentFullPath":"package/services/metadata/core-properties/","name":"629d5521ad934906a0d3c36051d35320.psmdcp","isDirectory":false},{"fullPath":"lib/net472/","parentFullPath":"lib/","name":"net472","isDirectory":true},{"fullPath":"lib/","parentFullPath":"/","name":"lib","isDirectory":true},{"fullPath":"_rels/","parentFullPath":"/","name":"_rels","isDirectory":true},{"fullPath":"_rels/.rels","parentFullPath":"_rels/","name":".rels","isDirectory":false},{"fullPath":"tools/","parentFullPath":"/","name":"tools","isDirectory":true},{"fullPath":"tools/chocolateyInstall.ps1","parentFullPath":"tools/","name":"chocolateyInstall.ps1","isDirectory":false},{"fullPath":"tools/chocolateyUninstall.ps1","parentFullPath":"tools/","name":"chocolateyUninstall.ps1","isDirectory":false},{"fullPath":"[Content_Types].xml","parentFullPath":"/","name":"[Content_Types].xml","isDirectory":false}]')} + + $invokeParallelScript = { + # This is what comes from Get-PackageInstallationData when it calls the invoke-parallel + param($objects) + + $newPackages = @() + foreach ($package in $objects) { + $packageInfoPath = "$global:globalRoot\..\TestFiles\PackageObjects\$($package.Name).json" + if (Test-Path $packageInfoPath) { + $packageInfo = Get-Content $packageInfoPath | ConvertFrom-Json + + $newPackages += @{ + ValidPackage = $true + Package = $packageInfo + } + } + } + + return @($newPackages) + } + + Mock Invoke-Parallel -ModuleName $moduleForMock -MockWith $invokeParallelScript + Mock -CommandName Invoke-Parallel -ModuleName "Alkami.PowerShell.Choco" -MockWith $invokeParallelScript + + # Define a good testable spread of packages. + # This is the list of packages that will be built based on the json files in \TestFiles\PackageObjects + # If you add a new package to the list, you need a new object there. See \TestFiles\readme.md for additional details. + $packageText = @" +# Installers +Alkami.MicroServices.Choco.Installer.Database 2.4.6 +Alkami.MicroServices.Choco.Installer.Logic 2.4.6 +Alkami.MicroServices.Choco.Installer.MasterDatabase 2.4.6 +Alkami.Installer.Provider 3.0.6 +Alkami.Installer.WebExtension 3.0.1 +Alkami.Installer.Widget 3.0.2 + +# Infrastructure +Alkami.MicroServices.Broker.Host 2.8.1 +Alkami.Services.Subscriptions.Host 3.5.2 + +# Providers +Alkami.App.Nag.Providers 1.1.4 +Alkami.App.Processor.Wire.FedwireOutput 1.2.3 +Alkami.App.Providers.CheckImaging.CorporateOne 1.0.1 +Alkami.App.Providers.Multiplexer.Client 1.0.4 +Alkami.App.Providers.Radium.BillPay 1.1.0 + +# Tier 1 Microservices +Alkami.MicroServices.Forms.Service.Host 1.1.1 +Alkami.MicroServices.Authorization.Service.Host 1.4.3 +Alkami.MicroServices.Security.Service.Host 2.17.2 +Alkami.MicroServices.UserInterface.Service.Host 1.17.0 +Alkami.MicroServices.Audit.Service.Host 6.11.0 +Alkami.MicroServices.Contacts.Service.Host 2.4.8 +Alkami.MicroServices.Holidays.Service.Host 2.1.12 +Alkami.MicroServices.Images.Service.Host 3.1.14 +Alkami.MicroServices.Notifications.Service.Host 1.6.12 +Alkami.MicroServices.Settings.Service.Host 4.4.0 +Alkami.MicroServices.SiteText.Service.Host 1.1.17 +Alkami.MicroServices.Transactions.Service.Host 1.7.1 +Alkami.MicroServices.EventManagement.Service.Host 3.4.9 + +# Tier 2 Microservices +Alkami.MicroServices.QuickApply.Service.Host 9.0.0 +Alkami.MicroServices.Registration.Service.Host 2.5.1 +Alkami.MicroServices.RemoteDeposit.Service.Host 2.4.1 +Alkami.MicroServices.CardManagementProviders.SymConnect.Host 3.7.1 + +# Powershell Modules +Alkami.Ops.Common 3.0.3 +Alkami.PowerShell.Choco 3.5.4 +Alkami.PowerShell.Common 3.2.6 + +# SDK +DS.AccountService.MS.Service.Host 1.0.9 +DS.BranchService.MS.Service.Host 1.0.10 +DS.CMN.MS.Service.Host 1.0.8 +DS.CopyRequestService.MS.Host 1.0.1 +DS.CoreService.MS.Service.Host 2.0.5 +DS.EStatements.MS.Service.Host 1.0.7 +DS.MarketingEmailService.MS.Host 1.0.7 +DS.MicroService.ContentService.Service.Host 2.0.4 +DS.SecureMessage.MS.Service.Host 1.0.10 +DS.Settings.MS.Service.Host 1.0.8 +DS.SkipAPayService.MS.Host 1.0.8 + +# Symitar +Alkami.MicroServices.AutoBiller.Symitar.Service.Host 2.1.4 +Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host 1.1.3 +Alkami.MicroServices.SymConnectMultiplexer.Service.Host 2.1.2 +Alkami.Providers.FakeSymitarProvider 1.2.3 +"@ + + Write-Host "format-parsing" + # Strip out the comment lines and parse the packages out into package objects. + $packageText = ($packageText -split "`r`n") | Where-Object { $_ -notlike "*#*" } + $packages = Format-ParseChocoPackages -text $packageText -delimiter " " + + # Define a fake feed URL + $fakeFeed = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + + # Attach fake source feeds onto each package, since we don't want to run this test against real nuget feeds. + foreach ($package in $packages) { + $feedMap = @{ + Source = $fakeFeed + IsSdk = $package.Name -like "DS.*" # Mark the desert schools packages as SDK. + } + $package.Feed = New-Object PSObject -Property $feedMap + } + + # Define fake PSCredential user. + $user = 'AlkamiFakeUser' + $pass = Get-SecureString 'abc123' + $credential = New-Object System.Management.Automation.PSCredential($user, $pass) + + # Define fake server lists. + $serversNonFabEnvironment = @("APP1234","APP1235","MIC1234","MIC1235","WEB1234","WEB1235") + + # Clone packages into $packages2 as a complete copy. + $ms = New-Object System.IO.MemoryStream + $bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + $bf.Serialize($ms, $packages) + $ms.Position = 0 + # Deep Copy Data + $packages2 = $bf.Deserialize($ms) + $ms.Close() + + Write-Host "get the dataaaaa" + # Run classification functions to be re-used for every test. + $nonFabPackageData = Get-PackageInstallationData -ChocoPackages $packages2 -Servers $serversNonFabEnvironment -NugetCredential $credential -UseV2PackageMetadata + Write-Host "did the dataaaaa" + + Context "Tier Assignment" { + It "Correctly Assigns Installer Packages to Tier 0" { + # All 6 installer packages defined above should be in tier 0 + $installers = $nonFabPackageData | Where-Object { $_.IsInstaller -and ($_.Tier -eq 0)} + $installers.Count | Should -Be 6 + } + + It "Correctly Assigns Infrastructure Packages to Tier 0" { + # Both infrastructure packages defined above should be in tier 0 + $installers = $nonFabPackageData | Where-Object { $_.IsInfrastructure -and ($_.Tier -eq 0)} + $installers.Count | Should -Be 2 + } + + # All packages assigned tiers + It "Assigns Tiers to Every Package" { + $badTierAssignments = $nonFabPackageData | Where-Object { ($null -eq $_.Tier) -or ($_.Tier -lt 0) } + $badTierAssignments.Count | Should -Be 0 + } + + It "Returns Packages Ordered by Tier" { + # Sweep through all of the packages, and make sure that the tier always increases & never decreases. + $lastPackage = $nonFabPackageData[0] + $isTierOrderingValid = $true + foreach ($package in $nonFabPackageData) { + $isTierOrderingValid = $isTierOrderingValid -and ($package.Tier -ge $lastPackage.Tier) + $lastPackage = $package + } + $isTierOrderingValid | Should -Be $true + } + } + + + Context "Test with PackageToServers non null" { + + $servers = $serversNonFabEnvironment + $micServers = Get-ServerByType -Type Mic -Server $servers + $appServers = Get-ServerByType -Type App -Server $servers + + # Dynamically generate some packaages with all and some packages without some servers + # Calculate the number of servers removed from the generated list + # Compare to the number of servers found missing from + + function Get-RandomServerList { + [CmdletBinding()] + param ( + $Servers + ) + + if ($Servers.IndexOf(',') -gt -1) { + $Servers = $Servers -split ',' + } + + if (Test-IsCollectionNullOrEmpty -Collection $servers) { + return $Servers + } + + if ($Servers.Count -eq 1) { + return $Servers + } + + return ($Servers | Sort-Object { Get-Random } | Select-Object -First (Get-Random -Minimum ($Servers.Count - 1) -Maximum ($Servers.Count + 1)) | Sort-Object) + } + + $packageMap = @{} + $removedPackageCount = 0 + # todo: build a packagemap from the packages so we can sort what isn't on a server + foreach ($package in $packages2) { + $removedCount = 0 + $packageLower = $package.Name.ToLower() + if ($packageLower -match "installer") { + $packageMap.$packageLower = $servers + } + elseif ($packageLower -match "fake") { + # fakes get ignored + } + elseif ($packageLower -match "powershell" -or $packageLower -match "\.ops\.") { + $packageMap.$packageLower = $servers + } + elseif ($packageLower -match "processor" -or $packageLower -match "\.providers") { + $serversCalculated = @(Get-RandomServerList $appServers) + $packageMap.$packageLower = $serversCalculated + $removedCount = $appServers.Count - $serversCalculated.Count + } + elseif ($packageLower -match "DS\.") { + $serversCalculated = @(Get-RandomServerList $micServers) + $packageMap.$packageLower = $serversCalculated + $removedCount = $micServers.Count - $serversCalculated.Count + } + elseif ($packageLower -eq "Alkami.MicroServices.Broker.Host" -or $packageLower -eq "Alkami.Services.Subscriptions.Host" -or $packageLower -eq "Alkami.MicroServices.Authorization.Service.Host") { + $serversCalculated = $servers + $packageMap.$packageLower = $serversCalculated + $removedCount = $servers.Count - $serversCalculated.Count + } + else { + $serversCalculated = @(Get-RandomServerList $micServers) + $packageMap.$packageLower = $serversCalculated + $removedCount = $micServers.Count - $serversCalculated.Count + } + if ($removedCount -gt 0) { +# Write-Host "$($package.Name) - $removedCount" + $removedPackageCount += $removedCount + } + } + + # Run classification functions to be re-used for every test. + $packageMapTestData = Get-PackageInstallationData -ChocoPackages $packages2 -Servers $serversNonFabEnvironment -NugetCredential $credential -UseV2PackageMetadata -PackageToServerMap $packageMap + + It "Got some results for missing server packages" { + $packageMapTestData.IsMissingFromServers -eq $true | Should -Not -BeNullOrEmpty + } + It "Should have removed as many servers as we expected" { + $missingCount = 0 + foreach ($package in $packageMapTestData) { + if ($package.MissingFromServers.Count -gt 0) { +# Write-Host "$($package.Name) - $($package.MissingFromServers.Count)" + $missingCount += $package.MissingFromServers.Count + } + } + $missingCount | Should -Be $removedPackageCount + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.ps1 new file mode 100644 index 0000000..aa7eaa5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageInstallationData.ps1 @@ -0,0 +1,249 @@ +function Get-PackageInstallationData { +<# +.SYNOPSIS +Returns an array of tiered packages to install, and attaches metadata to each package object about how the package should be installed. This does not modify the original $packages list. + +.PARAMETER ChocoPackages +Array of [object] Packages with Name, Version, and Feed info + +.PARAMETER Servers +Array of [string] Server hostnames used to decide which hosttypes should be signalled as targets for a given ChocoPackage + +.PARAMETER FIlterFeeds +Causes the filtering out of ChocoPackages whose Feed property is undesirable for a deploy; for example, "Default", nuget.internal, SRETools. + +.PARAMETER FilterPowerShellModules +Causes the filtering out of ChocoPackages that are PowerShellModules + +.PARAMETER NugetCredential +Credential information to access the nuget server + +.PARAMETER IncludeMissingPackages +Include packages not found on nuget server in the return object(s). +Bypass default Error output for missing packages. +Packages returned this way are duplicates of the package that was passed in, plus two members: IsValid=$false, Tier=Tier count + 1 + +.PARAMETER UseV2PackageMetadata + Use the new pacakge metadata v2 methods + +.PARAMETER PackageToServerMap + Optional map of packages to servers. Useful in TeamCity.Deployment.Code for determining if a package is missing from the servers in the environment +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [AllowNull()] + [object[]]$ChocoPackages, + [Parameter(Mandatory=$true)] + [string[]]$Servers, + [Parameter(Mandatory = $false)] + [switch]$FilterFeeds, + [Parameter(Mandatory = $false)] + [switch]$FilterPowerShellModules, + [Parameter(Mandatory=$true)] + [PSCredential]$NugetCredential, + [Parameter(Mandatory=$false)] + [switch]$IncludeMissingPackages, + [Parameter(Mandatory=$false)] + [switch]$UseV2PackageMetadata, + [object]$PackageToServerMap + ) + + $loglead = Get-LogLeadName + + if (!$ChocoPackages -or ($ChocoPackages.Count -eq 0)) { + return $null + } + + # Determine what types of servers are in the environment. + $webServers = Select-AlkamiWebServers $Servers + $appServers = Select-AlkamiAppServers $Servers + $micServers = Select-AlkamiMicServers $Servers + $fabServers = Select-AlkamiFabServers $Servers + $hasWebServers = $null -ne $webServers + $hasAppServers = $null -ne $appServers + $hasMicServers = $null -ne $micServers + $hasFabServers = $null -ne $fabServers + + # Select the first server to grab configured feeds from. + $firstServer = $Servers | Select-Object -First 1 + + # Verify that all of the packages have feeds set, to make the appropriate proget API calls. + if (([array]($ChocoPackages | Where-Object { $null -eq $_.Feed })).Count -gt 0) { + Set-ChocoPackageSourceFeedsV2 -Packages $ChocoPackages -Hostname $firstServer + } + # Filter out any packages that come from the default chocolatey feed, nuget.internal, or the SRE tools feed. + if ($filterFeeds.IsPresent) { + $ChocoPackages = $ChocoPackages | Where-Object { (!$_.Feed.IsDefault) -and ($_.Feed.Source -notlike "*nuget/nuget.internal*") -and ($_.Feed.Source -notlike "*SRETools*") } + } + + # Filter out PowerShell modules. + if ($filterPowerShellModules.IsPresent -and ($null -ne $chocoPackages)) { + # Test-IsPackagePowerShellModule tests against a global in \Alkami.PowerShell.Choco\Private\VariableDeclarations.ps1 + $ChocoPackages = $ChocoPackages | Where-Object { !(Test-IsPackagePowerShellModuleV2 -PackageName $_.Name) } + } + + # Fetch package metadata for each package. + # Parallelize this because it is making calls against Proget. + # TODO: Refactor this to use a splatted var + # Force the results into an array. Necessary for unit testing, and prevents PS unilaterally unboxing for no raisin. + $categorizedPackages = @() + + $categorizedPackages += Invoke-Parallel -objects $ChocoPackages -returnObjects -arguments ($NugetCredential, $UseV2PackageMetadata, $hasMicServers, $hasFabServers) -script { + param($package, $arguments) + $NugetCredential = $arguments[0] + $micServersPresent = $arguments[2] + $fabServersPresent = $arguments[3] + + # Fetch metadata for the package. + $categorizedPackage = Get-PackageMetadataV2 -Package $package -NugetCredential $NugetCredential -MicroserviceTierPresent:$micServersPresent -ServiceFabricTierPresent:$fabServersPresent + + # Return the results in a map so we can separate the real result from needless PSJob properties (such as RunspaceId) on every package. + return @{ Package = $categorizedPackage; ValidPackage = $categorizedPackage.IsValid; } + } + + $badPackages = $categorizedPackages | Where-Object { $_.ValidPackage -ne $true } + + [string]$errorText = $null + if (!(Test-IsCollectionNullOrEmpty $badPackages)) { + if ($IncludeMissingPackages) { + Write-Host "$logLead : Resultset will be returned with Missing Packages included." + } else { + foreach ($badPackage in $badPackages) { + $errorText += "$($badPackage.Package.Name)|$($badPackage.Package.Version)|$($badPackage.Package.Feed.Source)`n" + } + Write-Error "$logLead : These Packages Were not found: `n $errorText" + } + } + + # retrieve just the packages and discard the other information + $categorizedPackages = $categorizedPackages | ForEach-Object { $_.Package } + + # Run back through each package and figure out where the package should be installed. + foreach ($package in $categorizedPackages) { + # This didn't pick up like I expected it to + $package | Add-Member -NotePropertyName "Tier" -NotePropertyValue (-1) -Force + + # ensure if we chose that we should do a force-install that we stick to the types of servers present + # as-is, a package can be marked "force install" to both tiers if neither tier can be determined correctly + if ($package.ForceInstallToWebDetermination) { + $package.InstallToWeb = $package.InstallToWeb -and $hasWebServers + } + if ($package.ForceInstallToAppDetermination) { + $package.InstallToApp = $package.InstallToApp -and $hasAppServers + } + + # we don't need this loop because all this should be happening in Get-PackageMetadata + Write-Host "$loglead : Skipping re-categorization for [$($package.Name)]" + continue + } + + if (!(Test-IsCollectionNullOrEmpty $PackageToServerMap)) { + # Since we have some packages that can be mapped, let's find out if they are on all the servers + Write-Host "$logLead : Testing each package for correct server count in PackageToServerMap hashtable" + foreach ($package in $categorizedPackages) { + $lowerName = $package.Name.ToLower() + $packageMap = $PackageToServerMap[$lowerName] + if (!(Test-IsCollectionNullOrEmpty $packageMap)) { + # The value in the map is a non-empty array + if ($package.InstallToWeb -and $hasWebServers) { + # figure out which servers are not in the list + foreach ($server in $webServers) { + if ($packageMap -notcontains $server) { + Write-Host "$logLead : Could not find server [$server] in the packageMap for package [$lowerName] - package missing from server." + if ($null -eq $package.MissingFromServers) { + $package.MissingFromServers = @() + } + $package.MissingFromServers += $server + $package.IsMissingFromServers = $true + } + } + } + if ($package.InstallToApp -and $hasAppServers) { + # figure out which servers are not in the list + foreach ($server in $appServers) { + if ($packageMap -notcontains $server) { + Write-Host "$logLead : Could not find server [$server] in the packageMap for package [$lowerName]. Package missing from server." + if ($null -eq $package.MissingFromServers) { + $package.MissingFromServers = @() + } + $package.MissingFromServers += $server + $package.IsMissingFromServers = $true + } + } + } + if ($package.InstallToMic -and $hasMicServers) { + # figure out which servers are not in the list + foreach ($server in $micServers) { + if ($packageMap -notcontains $server) { + Write-Host "$logLead : Could not find server [$server] in the packageMap for package [$lowerName]. Package missing from server." + if ($null -eq $package.MissingFromServers) { + $package.MissingFromServers = @() + } + $package.MissingFromServers += $server + $package.IsMissingFromServers = $true + } + } + } + } + } + } + + # Get the microservice tiers. + $tiers = Get-MicroserviceTiers + + # Get a [packageName -> tier] mapping of the tiers. + $tierMap = @{} + for ($tier = 0; $tier -lt $tiers.Count; $tier++) { + $tierPackageNames = $tiers[$tier] + foreach ($packageName in $tierPackageNames) { + $tierMap[$packageName] = $tier + } + } + $catchAllTier = $tiers.Count + + # Assign each package to a tier. + foreach ($package in $categorizedPackages) { + # Assign installers to the first tier. + $tier = -1 + if ($package.IsInstaller) { + $tier = 0 + } elseif ($package.IsComponentizedWebApp) { + $tier = 1 + } else { + # Check if the package is specifically named as being in a tier. + if ($tierMap.ContainsKey($package.Name)) { + $tier = $tierMap[$package.Name] + } elseif ($package.IsValid) { + # Put it in the last tier. + $tier = $catchAllTier + } else { + # Missing(invalid) packages get (Number of Tiers + 1) so that + # The sorting expression will give them a negative number so + # that they are _obviously_ invalid + $tier = $catchAllTier + 1 + } + } + + $package.Tier = $tier + } + + # FUTURE US SRE-16834 Hotfix Tiering + # We may want to set all hotfixes to $tiers.Count + 1. This is a good place for that. + + # Sort packages. + # Tiers -> Installers -> Infrastructure Microservices -> Microservices -> Not-Microservices -> SDK + # Powers of 2 guarantee that the higher-precedent conditions will always take priority in sorting. + # The "catchAllTier - tier" is to make the tiers sort in ascending order. catchAllTier = tiers.count + $categorizedPackages = ($categorizedPackages | Sort-Object -Property @{ + Expression = { + 16 * [int]($catchAllTier - $_.Tier) + + 8 * [int]($_.IsInstaller) + + 4 * [int]($_.IsInfrastructure) + + 2 * [int]($_.IsMicroservice) + + 1 * [int](!($_.IsSDK)) + } + } -Descending) + + return $categorizedPackages +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.ps1 new file mode 100644 index 0000000..7c7b9bc --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.ps1 @@ -0,0 +1,431 @@ +function Get-PackageMetadataV2 { +<# +.SYNOPSIS + Returns the package object with metadata attached about what the package is. This does not modify the original $package object. + +.PARAMETER Package + An object originating in Format-ParseChocoPackages that has an assigned feed and other details + +.PARAMETER NugetCredential + If the feed we are talking to needs credentials, those should be supplied here + Notably this was historically used for SDK feeds, but may be used in the future for other feeds + This is because the SDK feeds should have been moved to a more accessible, non-authorization based feed for speed + +.OUTPUTS + Returns a Package object (unless the input object was null, then we return null) + The returned Package object should include additional details if it has been categorized +#> + [CmdletBinding()] + [OutputType([object])] + Param ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object]$Package, + [Parameter(Mandatory = $true)] + [PSCredential]$NugetCredential, + [switch]$MicroserviceTierPresent, + [switch]$ServiceFabricTierPresent + ) + + $loglead = Get-LogLeadName + + #region Guard clauses + if ($null -eq $Package) { + return $null + } + + # Verify that all of the packages have feeds set, to make the appropriate proget API calls. + if (($null -eq $Package.Feed) -or ([string]::IsNullOrWhiteSpace($Package.Feed.Source))) { + Write-Error "$loglead : Package $($Package.Name) is missing a feed source. Please make sure that you have executed Set-ChocoPackageSourceFeedsV2 against the package" + return $Package + } + + Write-Host "$loglead : Now looking up information for $($Package.Name)|$($Package.Version) in $($Package.Feed.Source)" + #endregion Guard clauses + + # Replace the name of the package with the name (and casing) from the nuspec. This solves casing consistency problems. + $nuspec = Get-PackageNuspecXmlV2 -Package $Package -Credential $NugetCredential + if ($null -eq $nuspec) { + # If we can't get the nuspec, this isn't a package we can change much about. Must be public? + Write-Host "$logLead : Could not find a nuspec for [$($Package.Name)], continuing" + return $Package + } + $Package.Name = $nuspec.package.metadata.id + + # Now that we have gotten this far, we have a valid package + $Package.IsValid = $true + + # Query for the files in the package for re-use. + $packageFiles = Get-PackageFileListV2 -Package $Package -Credential $NugetCredential + + # Some values can be calculated for all packages independently, some require package knowledge more than just files + $isSdk = if ($null -eq $Package.Feed.Source) { $false } else { $Package.Feed.IsSDK } + + ## Functionality not supported by manifests, driven by global variables + # Define packages that must only be installed in upgrade-mode. + $upgradeOnly = Test-IsPackageUpgradeOnlyV2 -Package $Package + $hasInfrastructureMigrations = Test-PackageHasInfrastructureMigrationsV2 -PackageFiles $packageFiles + $isInfrastructure = Test-IsPackageInfrastructureMicroserviceV2 -Package $Package + $isReliableService = Test-IsPackageReliableServiceV2 -PackageFiles $packageFiles -Package $Package + $isComponentizedWebApp = Test-IsPackageComponentizedWebApplication -Package $Package + $tags = ($nuspec.Package.Metadata.Tags -split " ").Where({ ![string]::IsNullOrWhiteSpace($_) }) + + # Default values + $applicationTypeName = $null + $newRelicAppName = $null + $hasAlkamiManifest = $false + $componentType = $null + $isDbms = $false + $hasDatabaseConfigFile = $false + $isMigrationPackage = $false + $manifestRuntime = "" + $isMicroservice = $false + $isInstaller = $false + $isAppTierWebApplication = $false + $isWebTierWebApplication = $false + $isPowerShellModule = $false + $isFullScaleMicroservice = $false + $installToWeb = $false + $installToApp = $false + $installToMic = $false + $installToFab = $false + $webAllowListed = $false + $appAllowListed = $false + $isReportPackage = $false + $isHotfixPackage = $false + $hasMigrations = $false + # We need a way to maintain this deep into the heirarchy well after classification, so adding it to the package seemed appropriate + $hotfixFixedInOrbVersion = "" + # Added to support hotfixes potentially not being uninstalled with scripts. + # Look for this to get updated in the Classify_Packages and honored in Install_Packages sections + $skipUninstallScripts = $false + # This is here to provide an easier way to infer the data elsewhere in the pipeline. + # See also: Remove-PackagesThatAreAlreadyInstalled + # Additionally, this function is the source-of-truth for how-it-should-be, so documentation as code is good. + $mustReinstallForOrbInstall = $false + + # we had an old toggle-setting problem that default installed packages to app, + # so we should force set to app tier if we don't have a tier _and_ it's a legacy classification + $usingLegacyValidation = $false + $forceToAppTier = $false + $forceToWebTier = $false + + #region This is where we decide if we have a file, and then make the results off of that + if ((Test-PackageHasAlkamiManifestV2 -PackageFiles $packageFiles)) { + $hasAlkamiManifest = $true + $alkamiManifest = Get-PackageAlkamiManifestV2 -Package $Package -Credential $NugetCredential + + $componentName = $alkamiManifest.general.element + $componentType = $alkamiManifest.general.componentType + Write-Host "$logLead : Categorizing [$($Package.Name)|$componentName] as a [$componentType] based on AlkamiManifest" + + switch ($componentType) { + 'WebApplication' { + # client web app (think CUFX, Isotope) + if ((Get-ValidWebTierInstallLocations) -contains $alkamiManifest.webApplicationManifest.appInstall) { + Write-Host "$logLead : WebApplication [$componentName] determined to be Web-tier" + $isWebTierWebApplication = $true + $installToWeb = $true + } + # WCF web app (think RPSTS) + if (@('Legacy') -contains $alkamiManifest.webApplicationManifest.appInstall) { + Write-Host "$logLead : WebApplication [$componentName] determined to be App-tier" + $isAppTierWebApplication = $true + $installToApp = $true + } + } + 'WebSite' { + # Websites are indistinguishable as they could be intended for web or app tiers + # Since they have to be mapped to from nginx or similar, and as they are few and far between, we just install them everywhere for now + # TODO: Add WebSite manifest concept for tiering (ex: CoreDashboard runs on the app tier) + $installToApp = $true + $installToWeb = $true + Write-Host "$logLead : Website [$componentName] found, categorized to go on both web and app tier" + } + 'Widget' { + $installToWeb = $true + $mustReinstallForOrbInstall = $true + } + 'WebExtension' { + $installToWeb = $true + $mustReinstallForOrbInstall = $true + } + 'SREModule' { + $isPowerShellModule = $true + } + 'Report' { + # reports should do nothing, so don't set any environment flags + $isReportPackage = $true + # In case we determine we should uninstall it, just rapidly get it off the servers + $skipUninstallScripts = $true + } + 'Service' { + $isMicroservice = $true + $isDbms = Test-ServiceManifestRequiresDbAccess -ServiceManifest $alkamiManifest.serviceManifest + $manifestRuntime = Get-ValidatedRuntimeParameter -Runtime $alkamiManifest.serviceManifest.runtime + Write-Host "$logLead : Found service [$componentName] should $(if ($isDbms){}else{"not "} )run as a database capable user" + $hasMigrations = Test-ServiceManifestHasMigrations -ServiceManifest $alkamiManifest.serviceManifest + Write-Host "$logLead : Found service [$componentName] should $(if ($hasMigrations){}else{"not "} )run migrations" + } + 'Provider' { + $installToApp = $true + $mustReinstallForOrbInstall = $true + } + 'Installer' { + $isInstaller = $true + } + 'FluentMigration' { + $isMigrationPackage = $true + $hasMigrations = $true + $manifestRuntime = Get-ValidatedRuntimeParameter -Runtime $alkamiManifest.fluentMigrationManifest.runtime + } + 'NodeMigration' { + # This package type is here to demonstrate adding support for node based migration packages + $isMigrationPackage = $true + $hasMigrations = $true + throw "$logLead : There is no support for NodeMigrations yet. Please talk to SRE about removing this throw if you believe NodeMigrations should be supported in your environment." + } + 'LegacyUtility' { + # The type of utilities typically represented by this installer + # have historically only gone to the app tier, where the WCF services are + $installToApp = $true + } + 'Hotfix' { + # Hotfixes can go to APP OR WEB OR ALL + $isHotfixPackage = $true + $hotfixFixedInOrbVersion = $alkamiManifest.hotfixManifest.fixedInOrbVersion + $mustReinstallForOrbInstall = $true + + $hotfixServerTier = $alkamiManifest.hotfixManifest.serverTier + + # TODO: Will need to be revisited after 2022.4 release in the future (see also Get-PackageMetadataV2) + # Original 1.0 manifests did not have serverTier + # Null-or-Whitespace will mean Install-to-All-tiers until 2022.4 when hotfixes have serverTier node + if (Test-StringIsNullOrWhiteSpace -Value $hotfixServerTier) { + Write-Warning "$logLead : MISSING NODE - HOTFIX MANIFEST - serverTier" + Write-Warning "$logLead : Temporarily forcing this to install to ALL serverTiers" + Write-Warning "$logLead : This will be REMOVED in the NEAR future" + $hotfixServerTier = "ALL" + } + $installToApp = $hotfixServerTier -in ("APP", "ALL") + $installToWeb = $hotfixServerTier -in ("WEB", "ALL") + + # SRE-18492 - For Zero-downtime releases of ORB, where we are deploying FULL-ORB via a HOTFIX package + # - don't give me that look - + # we need to keep MICs in sync with every other host in the designation + # for that reason, we will keep MICs in sync with APPs + # we could be cute here and only do this when it's going to "ALL" or both tiers + # but cute is as bad as clever + # we could also simply set $installToMic, but I want it to be explicit and obvious below + # This will be used below where $installToMic gets set + $isHotfixInstallToApp = $installToApp + } + 'ApiComponent' { + # Api Components always go to web, that's the business rule + $installToWeb = $true + } + } + } else { + Write-Host "$logLead : Categorizing [$($Package.Name)] based on legacy criteria" + $usingLegacyValidation = $true + + # do the legacy checks that were developed over years of misconfiguration + # Define defaults for types of services/installations. + # Functions that specify -Credential are querying out to proget. + $isMicroservice = Test-IsPackageMicroserviceV2 -NuspecXmlObject $nuspec -Package $Package + $isInstaller = Test-IsPackageInstallerV2 -Package $Package + $isPowerShellModule = Test-IsPackagePowerShellModuleV2 -Package $Package + $installToWeb = ($_WebNuspecTags.Where({ $tags -contains $_ }).Count -gt 0) + $installToApp = ($_AppNuspecTags.Where({ $tags -contains $_ }).Count -gt 0) + $webAllowListed = $_WebAllowList -contains $Package.Name + $appAllowListed = $_AppAllowList -contains $Package.Name + + # If it's a microservice see if it's fullscale, etc + if ($isMicroservice) { + $isFullScaleMicroservice = Test-IsPackageFullScaleMicroserviceV2 -PackageName $Package.Name + + $isDbms = Test-IsPackageDbmsV2 -NuspecXmlObject $nuspec + $hasDatabaseConfigFile = Test-PackageHasDatabaseConfigFile -PackageFiles $packageFiles + + $hasMigrations = $isDbms -and $hasDatabaseConfigFile + } + } + #endregion This is where we decide if we have a file, and then make the results off of that + + # Some things have to be done once we have the categorized data no matter what + # If it's a microservice fetch the new relic app name + # This applies whether we have the manifest or not, same operation applies + if ($isMicroservice -or $isWebTierWebApplication -or $isAppTierWebApplication) { + $packagePaths = @($packageFiles.Where({ $_.name -eq "$($Package.Name).exe.config" }).fullPath) + if ($packagePaths.Count -eq 0) { + Write-Host "$logLead : Could not find a [$($Package.Name).exe.config] in proget, this is probably all fine and normal. Just means we can't set the NewRelic.AppName variable." + continue + } + $packagePath = $packagePaths[0] + + # in the case of more than + if ($packagePaths.Count -gt 1) { + # Find the one with the least number of splits (closest to the parent of the folder) + $packagePath = ($packagePaths | Sort-Object -Property @{Expression = { ($_ -split '/').Count }; Descending = $False } | Select-Object -First 1) + } + if (![string]::IsNullOrWhiteSpace($packagePath)) { + Write-Host "$logLead : Looking for appsettings for [$($Package.Name)] at [$packagePath]" + $appSettingsXml = [xml](Get-PackageFileV2 -Package $Package -Credential $NugetCredential -PackagePath $packagePath) + $newRelicAppName = Get-AppSetting -Key "NewRelic.AppName" -XmlDocument $appSettingsXml + } + } + + if ($isReliableService) { + $applicationTypeName = Get-AlkamiServiceFabricPackageServiceTypeName -source $Package.Feed.Source -name $package.Name -version $package.Version -nugetCredential $NugetCredential + } + + #region Determine tiering + # Figure out if it's something that goes on every server. + $goesEverywhere = $isInfrastructure -or $isInstaller -or $isPowerShellModule + + # Force the upgrade in certain scenarios, to ensure things are re-registered + # That is NOT what this does. This does NOT force anything + # This will NOT cause ANYTHING to be re-registered + # This is ONLY used in this file when negated - to set the "ForceSameVersion" member and + # not negated to set a member that is never used again, "Upgrade" + # "ForceSameVersion" is only used in Orb Installs - meaning jobs where "ForceReinstallPackages" is true + # + # So... + # Microservices do not need to be ForceSameVersion/ForceReinstallPackages - they do not get deleted with ORB + # Installers - same thing + # UpgradeOnly? That's just VariableDeclarations.ps1 -> $_UpgradePackages + # - PSModules, CoreDashboard, EagleEye, newrelic. Stuff well away from ORB folders + # Basically, this variable is "stuff that isn't blown away by an ORB deploy" + $isSafeFromOrbDeploys = $isMicroservice -or $upgradeOnly -or $isInstaller + $isDeletedByOrbDeploys = -NOT $isSafeFromOrbDeploys + + # appAllowListed means it is absolutely set to go to the app tier, which means it can't go to the web tier + # webAllowListed conversely means it can't go to the app tier + # In either case, if those are set to true, the corresponding tier must be set to false + # Example cases of these values being set are known SDK client packages that have odd or miscategorized names + + $installToWeb = ($goesEverywhere -or $isWebTierWebApplication -or $installToWeb -or $webAllowListed) + $installToApp = ($goesEverywhere -or $isAppTierWebApplication -or $installToApp -or $appAllowListed -or $isFullScaleMicroservice) + $installToMic = ($goesEverywhere -or $installToMic -or $isMicroservice -or $isFullScaleMicroservice -or $isHotfixInstallToApp) + $installToFab = ($goesEverywhere -or $installToFab -or $isReliableService -or $isFullScaleMicroservice) + + # If it's a service fabric server, we should put the microservice tier packages there + if ($ServiceFabricTierPresent) { + $installToFab = ($installToFab -or $installToMic) + $installToMic = $false + } else { + $installToFab = $false + } + + if (!$MicroserviceTierPresent) { + # If there's no microservice tier, whatever was gonna go on mics should goto apps + $installToApp = $installToApp -or $installToMic + $installToMic = $false + } + + if ($usingLegacyValidation -and !$installToWeb -and !$installToApp -and !$installToMic -and !$installToFab) { + # we had an old toggle-setting problem that default installed packages to app, + # so we should force set to web and app tiers if we don't have a tier _and_ it's a legacy classification + $forceToAppTier = $true + $forceToWebTier = $true + } + #endregion Determine tiering + + #region Build the output object + # Create a new package object, and copy over all of the existing data. + $newPackageData = @{ } + foreach ($property in $Package.psobject.properties.name) { + $newPackageData[$property] = $Package.$property + } + + # Store classification metadata. + $newPackageData["IsSDK"] = $isSdk + $newPackageData["IsHotfix"] = $isHotfixPackage + if ($isHotfixPackage) { + $newPackageData["HotfixFixedInOrbVersion"] = $hotfixFixedInOrbVersion + $newPackageData["ServerTier"] = $hotfixServerTier + } + $newPackageData["SkipUninstallScripts"] = $skipUninstallScripts + $newPackageData["HasAlkamiManifest"] = $hasAlkamiManifest + if ($hasAlkamiManifest) { + $newPackageData["ComponentType"] = $componentType + } + $newPackageData["IsInstaller"] = $isInstaller + $newPackageData["IsReportPackage"] = $isReportPackage + $newPackageData["IsInfrastructure"] = $isInfrastructure + $newPackageData["IsComponentizedWebApp"] = $isComponentizedWebApp + $newPackageData["NewRelicAppName"] = $newRelicAppName + $newPackageData["Tags"] = $tags + + # Some things have to be reinstalled if we reinstall ORB legacy + if ($mustReinstallForOrbInstall) { + $newPackageData["ReinstallWithORB"] = $mustReinstallForOrbInstall + } + + # x - Track things that are only installed when upgrading + # This ^ is not worded well. This is things that are not blown away by an ORB deploy + # This is also NOT USED anywhere. + $newPackageData["Upgrade"] = $isSafeFromOrbDeploys + # Reinstall the things that get blown away by ORB deploys + $newPackageData["ForceSameVersion"] = $isDeletedByOrbDeploys + $newPackageData["PreventRollback"] = $isInstaller + $newPackageData["HasInfrastructureMigration"] = $hasInfrastructureMigrations + + # Store database related metadata + $newPackageData["HasMigrations"] = $hasMigrations + $newPackageData["IsDbms"] = $isMicroservice -and $isDbms + $newPackageData["IsMigrationPackage"] = $isMigrationPackage + + # SRE-16977 MigrationRunner utility required metadata + if ($hasAlkamiManifest -and ($isDbms -or $isMigrationPackage)) { + $newPackageData["ManifestRuntime"] = $manifestRuntime + $newPackageData["MigrationRunnerPath"] = Get-MigrationRunnerExe -Runtime $manifestRuntime + } + + # Store microservice related metdata + $newPackageData["IsMicroservice"] = $isMicroservice + $newPackageData["IsFullScaleMicroservice"] = $isFullScaleMicroservice + + # Add Service Fabric metadata. + $newPackageData["IsReliableService"] = $isReliableService + if (![string]::IsNullOrWhiteSpace($applicationTypeName)) { + $newPackageData["ApplicationTypeName"] = $applicationTypeName + } + + # Store Install-To-Tier Metadata + $newPackageData["InstallToWebTier"] = $installToWeb -or $forceToWebTier + $newPackageData["InstallToAppTier"] = $installToApp -or $forceToAppTier + # we had an old toggle-setting problem that default installed packages to app, + # so we should force set to web and app tiers if we don't have a tier _and_ it's a legacy classification + # see code above + $newPackageData["ForceInstallToWebTierDetermination"] = $forceToWebTier + $newPackageData["ForceInstallToAppTierDetermination"] = $forceToAppTier + $newPackageData["InstallToMicTier"] = $installToMic + + # This categorization is bypassed in the Get-PackageInstallationData step, so we move it here. + $newPackageData["InstallToWeb"] = $installToWeb -or $forceToWebTier + $newPackageData["InstallToApp"] = $installToApp -or $forceToAppTier + + # This information should be populated by a process that knows about what servers and packages are in an environment + # The "IsMissingFromServers" should be set to bool based on the above information by whoever sets that value + # The properties are defined here, however, as "class definition" so we know where they are initialized from when searching the codebase. + $newPackageData["IsMissingFromServers"] = $null + $newPackageData["MissingFromServers"] = @() + + # we had an old toggle-setting problem that default installed packages to app, + # so we should force set to web and app tiers if we don't have a tier _and_ it's a legacy classification + # see code above + $newPackageData["ForceInstallToWebDetermination"] = $forceToWebTier + $newPackageData["ForceInstallToAppDetermination"] = $forceToAppTier + $newPackageData["InstallToMic"] = $installToMic + + # There are no dynamically determined fabric clusters anymore. We know where we are intentionally deploying to fab instead of mic. + $newPackageData["InstallToFab"] = $installToFab + + $newPackageData["Tier"] = -1 + + # Create the new package object and return; + $newPackage = New-Object -TypeName PSObject -Prop $newPackageData + return $newPackage +#endregion Build the output object +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.tests.ps1 new file mode 100644 index 0000000..38c9cbb --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageMetadataV2.tests.ps1 @@ -0,0 +1,792 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +. $PSScriptRoot\..\TestFiles\Write-OrderedJson.ps1 + +Describe "Get-PackageMetadata for legacy" { + + # Global Mocks + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-MigrationRunnerExe -ModuleName $moduleForMock -MockWith { + return "C:\temp\FakeMigrationRunner.exe" + } + + Mock Get-PackageNuspecXmlV2 -ModuleName $moduleForMock { + param($Package, $Credential) + $name = $Package.Name + return (New-Object PSObject -Property @{ package = @{ metadata = @{ id = $name } } }) + } + + # this long magic string is a barebones package folder with no useful contents. Just the basic nuget structure and a choco install file set + Mock Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { return (ConvertFrom-Json '[{"fullPath":"package/services/metadata/core-properties/","parentFullPath":"package/services/metadata/","name":"core-properties","isDirectory":true},{"fullPath":"package/services/metadata/","parentFullPath":"package/services/","name":"metadata","isDirectory":true},{"fullPath":"package/services/","parentFullPath":"package/","name":"services","isDirectory":true},{"fullPath":"package/","parentFullPath":"/","name":"package","isDirectory":true},{"fullPath":"package/services/metadata/core-properties/629d5521ad934906a0d3c36051d35320.psmdcp","parentFullPath":"package/services/metadata/core-properties/","name":"629d5521ad934906a0d3c36051d35320.psmdcp","isDirectory":false},{"fullPath":"lib/net472/","parentFullPath":"lib/","name":"net472","isDirectory":true},{"fullPath":"lib/","parentFullPath":"/","name":"lib","isDirectory":true},{"fullPath":"_rels/","parentFullPath":"/","name":"_rels","isDirectory":true},{"fullPath":"_rels/.rels","parentFullPath":"_rels/","name":".rels","isDirectory":false},{"fullPath":"tools/","parentFullPath":"/","name":"tools","isDirectory":true},{"fullPath":"tools/chocolateyInstall.ps1","parentFullPath":"tools/","name":"chocolateyInstall.ps1","isDirectory":false},{"fullPath":"tools/chocolateyUninstall.ps1","parentFullPath":"tools/","name":"chocolateyUninstall.ps1","isDirectory":false},{"fullPath":"[Content_Types].xml","parentFullPath":"/","name":"[Content_Types].xml","isDirectory":false}]')} + + Mock -CommandName Get-PackageFileV2 -ModuleName $moduleForMock -MockWith {} + + # Define fake PSCredential user. + $fakeCredential = New-Object System.Management.Automation.PSCredential('AlkamiFakeUser', (Get-SecureString 'abc123')) + + # Define a good testable spread of packages. + # This is the list of packages that will be built based on the json files in \TestFiles\PackageObjects + # If you add a new package to the list, you need a new object there. See \TestFiles\readme.md for additional details. + $packageText = @" +# Installers +Alkami.MicroServices.Choco.Installer.Database 2.4.6 +Alkami.MicroServices.Choco.Installer.Logic 2.4.6 +Alkami.MicroServices.Choco.Installer.MasterDatabase 2.4.6 +Alkami.Installer.Provider 3.0.6 +Alkami.Installer.WebExtension 3.0.1 +Alkami.Installer.Widget 3.0.2 + +# Infrastructure +Alkami.MicroServices.Broker.Host 2.8.1 +Alkami.Services.Subscriptions.Host 3.9.0 + +# Providers +Alkami.App.Nag.Providers 1.1.4 +Alkami.App.Processor.Wire.FedwireOutput 1.6.0 +Alkami.App.Providers.CheckImaging.CorporateOne 1.0.1 +Alkami.App.Providers.Multiplexer.Client 1.0.4 +Alkami.App.Providers.Radium.BillPay 1.1.0 + +# Tier 1 Microservices +Alkami.MicroServices.Forms.Service.Host 1.1.1 +Alkami.MicroServices.Authorization.Service.Host 1.4.3 +Alkami.MicroServices.Security.Service.Host 2.17.2 +Alkami.MicroServices.UserInterface.Service.Host 1.17.0 +Alkami.MicroServices.Audit.Service.Host 6.11.0 +Alkami.MicroServices.Contacts.Service.Host 2.4.8 +Alkami.MicroServices.Holidays.Service.Host 2.1.12 +Alkami.MicroServices.Images.Service.Host 3.1.14 +Alkami.MicroServices.Notifications.Service.Host 1.6.12 +Alkami.MicroServices.Settings.Service.Host 4.4.0 +Alkami.MicroServices.SiteText.Service.Host 1.1.17 +Alkami.MicroServices.Transactions.Service.Host 1.7.1 +Alkami.MicroServices.EventManagement.Service.Host 3.4.9 + +# Tier 2 Microservices +Alkami.MicroServices.QuickApply.Service.Host 9.0.0 +Alkami.MicroServices.Registration.Service.Host 2.5.1 +Alkami.MicroServices.RemoteDeposit.Service.Host 2.4.1 +Alkami.MicroServices.CardManagementProviders.SymConnect.Host 3.7.1 + +# Powershell Modules +Alkami.Ops.Common 3.0.3 +Alkami.PowerShell.Choco 3.5.4 +Alkami.PowerShell.Common 3.2.6 + +# SDK +DS.AccountService.MS.Service.Host 1.0.9 +DS.BranchService.MS.Service.Host 1.0.10 +DS.CMN.MS.Service.Host 1.0.8 +DS.CopyRequestService.MS.Host 1.0.1 +DS.CoreService.MS.Service.Host 2.0.5 +DS.EStatements.MS.Service.Host 1.0.7 +DS.MarketingEmailService.MS.Host 1.0.7 +DS.MicroService.ContentService.Service.Host 2.0.4 +DS.SecureMessage.MS.Service.Host 1.0.10 +DS.Settings.MS.Service.Host 1.0.8 +DS.SkipAPayService.MS.Host 1.0.8 + +# Symitar +Alkami.MicroServices.AutoBiller.Symitar.Service.Host 2.1.4 +Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host 1.1.3 +Alkami.MicroServices.SymConnectMultiplexer.Service.Host 2.1.2 +Alkami.Providers.FakeSymitarProvider 1.2.3 +"@; + + # Strip out the comment lines and parse the packages out into package objects. + $packageText = ($packageText -split "`r`n") | Where-Object { $_ -notlike "*#*"} + $packages = Format-ParseChocoPackages -text $packageText -delimiter " " + + # Attach fake source feeds onto each package, since we don't want to run this test against real nuget feeds. + foreach($package in $packages) + { + $feedMap = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $package.Name -like "DS.*" # Mark the desert schools packages as SDK. + } + $package.Feed = New-Object PSObject -Property $feedMap + $package.Version = "1.2.3" + } + + # Run classification function on all of the packages. + $nonFabPackageData = $packages | ForEach-Object { Get-PackageMetadataV2 -Package $_ -NugetCredential $fakeCredential } + + Context "Package Classification" { + + It "Determines Microservices to be Upgrade Only" { + $nonUpgradeMicroservices = $nonFabPackageData | Where-Object { $_.IsMicroservice -and (!$_.Upgrade) } + $nonUpgradeMicroservices.Count | Should -be 0 + } + + It "Determines Non-Microservice Upgrade-Only Packages to be Upgrade Only" { + # Make sure that all of the Test-IsPackageUpgradeOnly positive packages come out with "Upgrade == $true" + $upgradeOnlyPackages = $nonFabPackageData | Where-Object { (!$_.IsMicroservice) -and (Test-IsPackageUpgradeOnlyV2 -Package $_) } + $badUpgradePackages = $upgradeOnlyPackages | Where-Object { $_.Upgrade -ne $true } + $badUpgradePackages | Should -be $null + } + + It "Determines Infrastructure Packages to be Infrastructure Packages" { + $badInfraPackages = $nonFabPackageData | Where-Object { (Test-IsPackageInfrastructureMicroserviceV2 -Package $_) -and (!$_.IsInfrastructure) } + $badInfraPackages | Should -be $null + } + + It "Determines Installer Packages to be Installer Packages" { + $badInstallerPackages = $nonFabPackageData | Where-Object { (Test-IsPackageInstallerV2 -Package $_) -and (!$_.IsInstaller) } + $badInstallerPackages | Should -be $null + } + + It "Determines Installer Packages to be Upgrade Only" { + $nonUpgradeInstallers = $nonFabPackageData | Where-Object { (Test-IsPackageInstallerV2 -Package $_) -and (!$_.Upgrade) } + $nonUpgradeInstallers.Count | Should -be 0 + } + + It "Determines Microservice Packages to be Microservice Packages" { + $badMicroPackages = $nonFabPackageData | Where-Object { (Test-IsPackageMicroserviceV2 -NuspecXmlObject $null -Package $_) -and (!$_.IsMicroservice) } + $badMicroPackages = $badMicroPackages | Where-Object { !$_.IsSdk } # Consequence of how the sdk packages are determined in these tests. + $badMicroPackages | Should -be $null + } + + It "Determines Database Packages to be Database Packages" { + $badDbmsPackages = $nonFabPackageData | Where-Object { (Test-IsPackageDbmsV2 -NuspecXmlObject $null) -and (!$_.IsDbms) } + $badDbmsPackages | Should -be $null + } + + It "Determines SDK Packages to be SDK Packages" { + $badSdkPackages = $nonFabPackageData | Where-Object { ($_.Feed.IsSDK) -and (!$_.IsSDK) } + $badSdkPackages | Should -be $null + } + } +} + +Describe "Get-PackageMetadata with Manifests" { + # Global Mocks + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-MigrationRunnerExe -ModuleName $moduleForMock -MockWith { + return "C:\temp\FakeMigrationRunner.exe" + } + + + # Define fake PSCredential user. + $fakeCredential = New-Object System.Management.Automation.PSCredential('AlkamiFakeUser', (Get-SecureString 'abc123')) + + + Mock Get-PackageNuspecXmlV2 -ModuleName $moduleForMock { + param($Package, $Credential) + $name = $Package.Name + return (New-Object PSObject -Property @{ package = @{ metadata = @{ id = $name } } }) + } + + # this long magic string is a barebones package folder with no useful contents. Just the basic nuget structure and a choco install file set + Mock Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { return (ConvertFrom-Json '[{"fullPath":"/AlkamiManifest.xml","parentFullPath":"/","name":"AlkamiManifest.xml","isDirectory":false},{"fullPath":"package/services/metadata/core-properties/","parentFullPath":"package/services/metadata/","name":"core-properties","isDirectory":true},{"fullPath":"package/services/metadata/","parentFullPath":"package/services/","name":"metadata","isDirectory":true},{"fullPath":"package/services/","parentFullPath":"package/","name":"services","isDirectory":true},{"fullPath":"package/","parentFullPath":"/","name":"package","isDirectory":true},{"fullPath":"package/services/metadata/core-properties/629d5521ad934906a0d3c36051d35320.psmdcp","parentFullPath":"package/services/metadata/core-properties/","name":"629d5521ad934906a0d3c36051d35320.psmdcp","isDirectory":false},{"fullPath":"lib/net472/","parentFullPath":"lib/","name":"net472","isDirectory":true},{"fullPath":"lib/","parentFullPath":"/","name":"lib","isDirectory":true},{"fullPath":"_rels/","parentFullPath":"/","name":"_rels","isDirectory":true},{"fullPath":"_rels/.rels","parentFullPath":"_rels/","name":".rels","isDirectory":false},{"fullPath":"tools/","parentFullPath":"/","name":"tools","isDirectory":true},{"fullPath":"tools/chocolateyInstall.ps1","parentFullPath":"tools/","name":"chocolateyInstall.ps1","isDirectory":false},{"fullPath":"tools/chocolateyUninstall.ps1","parentFullPath":"tools/","name":"chocolateyUninstall.ps1","isDirectory":false},{"fullPath":"[Content_Types].xml","parentFullPath":"/","name":"[Content_Types].xml","isDirectory":false}]')} + + Mock -CommandName Get-PackageFileV2 -ModuleName $moduleForMock -MockWith {} + + Context "Is a service" { + $package = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'StickyService' + Version = '999.999.999' + } + + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'StickyService' + componentType = 'Service' + } + serviceManifest = @{ + runtime = 'framework' + } + } + } + Mock -CommandName Get-ValidatedRuntimeParameter -ModuleName $moduleForMock -MockWith {return "framework"} + Mock -CommandName Test-ServiceManifestRequiresDbAccess -ModuleName $moduleForMock -MockWith {$false} + $classifiedPackage = Get-PackageMetadataV2 -Package $package -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is not going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeFalse + } + + It "Is not going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeFalse + } + + It "Is going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsMicroservice'" { + $classifiedPackage.IsMicroservice | Should -BeTrue + } + + It "Is not a dbms service" { + # because our fake service doesn't have a serviceManifest definition, much less declares that it needs dbms access + $classifiedPackage.IsDbms | Should -BeFalse + } + + It "Is not IsSDK" { + $classifiedPackage.IsSDK | Should -BeFalse + } + } + + Context "Is a service with migrations" { + $package = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $true + } + Name = 'StickyService' + Version = '999.999.999' + } + + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'StickyService' + componentType = 'Service' + } + serviceManifest = @{ + runtime = 'framework' + db_role = "totally_fake_role" + } + } + } + Mock -CommandName Get-ValidatedRuntimeParameter -ModuleName $moduleForMock -MockWith {return "framework"} + Mock -CommandName Test-ServiceManifestRequiresDbAccess -ModuleName $moduleForMock -MockWith {$true} + + $classifiedPackage = Get-PackageMetadataV2 -Package $package -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is not going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeFalse + } + + It "Is not going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeFalse + } + + It "Is going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsMicroservice'" { + $classifiedPackage.IsMicroservice | Should -BeTrue + } + + It "Is a dbms service" { + # because our fake service doesn't have a serviceManifest definition, much less declares that it needs dbms access + $classifiedPackage.IsDbms | Should -BeTrue + } + + It "Is IsSDK" { + $classifiedPackage.IsSDK | Should -BeTrue + } + } + + Context "Is a legacy WebApplication" { + $package = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'StickyWebApplication' + Version = '999.999.999' + } + + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { return @{ general = @{ element = 'StickyWebApplication'; componentType = 'WebApplication'}; webApplicationManifest = @{ appInstall = "Legacy" } } } + + $classifiedPackage = Get-PackageMetadataV2 -Package $package -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is not going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeFalse + } + + It "Is going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeTrue + } + + It "Is not going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeFalse + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is not classified 'IsMicroservice'" { + $classifiedPackage.IsMicroservice | Should -BeFalse + } + + It "Is not a dbms service" { + # because our fake service doesn't have a serviceManifest definition, much less declares that it needs dbms access + $classifiedPackage.IsDbms | Should -BeFalse + } + + It "Is not IsSDK" { + $classifiedPackage.IsSDK | Should -BeFalse + } + } + + Context "Is a web tier WebApplication" { + $package = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'StickyWebApplication' + Version = '999.999.999' + } + + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { return @{ general = @{ element = 'StickyWebApplication'; componentType = 'WebApplication'}; webApplicationManifest = @{ appInstall = "Generic" } } } + + $classifiedPackage = Get-PackageMetadataV2 -Package $package -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeTrue + } + + It "Is not going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeFalse + } + + It "Is not going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeFalse + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is not classified 'IsMicroservice'" { + $classifiedPackage.IsMicroservice | Should -BeFalse + } + + It "Is not a dbms service" { + # because our fake service doesn't have a serviceManifest definition, much less declares that it needs dbms access + $classifiedPackage.IsDbms | Should -BeFalse + } + + It "Is not IsSDK" { + $classifiedPackage.IsSDK | Should -BeFalse + } + } + + Context -Name "OrbHotfix with NO serverTier" { + $hotfixPackage = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'FakeHotfixPackage' + Version = '999.999.999' + } + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'HotfixDooDad'; + componentType = 'Hotfix' + }; + hotfixManifest = @{ + targetVersion = "ORB.VERSION" + } + } + } + + $classifiedPackage = Get-PackageMetadataV2 -Package $hotfixPackage -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeTrue + } + + It "Is going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeTrue + } + + It "Is going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsHotfix'" { + $classifiedPackage.IsHotfix | Should -BeTrue + } + } + + Context -Name "OrbHotfix with EMPTY STRING serverTier" { + $hotfixPackage = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'FakeHotfixPackage' + Version = '999.999.999' + } + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'HotfixDooDad'; + componentType = 'Hotfix' + }; + hotfixManifest = @{ + targetVersion = "ORB.VERSION"; + serverTier = "" + } + } + } + + $classifiedPackage = Get-PackageMetadataV2 -Package $hotfixPackage -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is going to web tier" { + $classifiedPackage.InstallToWebTier | Should -BeTrue + } + + It "Is going to app tier" { + $classifiedPackage.InstallToAppTier | Should -BeTrue + } + + It "Is going to mic tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsHotfix'" { + $classifiedPackage.IsHotfix | Should -BeTrue + } + } + + Context -Name "OrbHotfix with APP serverTier" { + $hotfixPackage = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'FakeHotfixPackage' + Version = '999.999.999' + } + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'HotfixDooDad'; + componentType = 'Hotfix' + }; + hotfixManifest = @{ + targetVersion = "ORB.VERSION"; + serverTier = "APP" + } + } + } + + $classifiedPackage = Get-PackageMetadataV2 -Package $hotfixPackage -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is NOT going to WEB tier" { + $classifiedPackage.InstallToWebTier | Should -BeFalse + } + + It "Is going to APP tier" { + $classifiedPackage.InstallToAppTier | Should -BeTrue + } + + It "Is going to MIC tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsHotfix'" { + $classifiedPackage.IsHotfix | Should -BeTrue + } + + } + Context -Name "OrbHotfix with WEB serverTier" { + $hotfixPackage = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'FakeHotfixPackage' + Version = '999.999.999' + } + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'HotfixDooDad'; + componentType = 'Hotfix' + }; + hotfixManifest = @{ + targetVersion = "ORB.VERSION"; + serverTier = "WEB" + } + } + } + + $classifiedPackage = Get-PackageMetadataV2 -Package $hotfixPackage -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is going to WEB tier" { + $classifiedPackage.InstallToWebTier | Should -BeTrue + } + + It "Is NOT going to APP tier" { + $classifiedPackage.InstallToAppTier | Should -BeFalse + } + + It "Is NOT going to MIC tier" { + $classifiedPackage.InstallToMicTier | Should -BeFalse + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsHotfix'" { + $classifiedPackage.IsHotfix | Should -BeTrue + } + + } + Context -Name "OrbHotfix with ALL serverTier" { + $hotfixPackage = @{ + Feed = @{ + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + IsSdk = $false + } + Name = 'FakeHotfixPackage' + Version = '999.999.999' + } + Mock -CommandName Get-PackageAlkamiManifestV2 -ModuleName $moduleForMock -MockWith { + return @{ + general = @{ + element = 'HotfixDooDad'; + componentType = 'Hotfix' + }; + hotfixManifest = @{ + targetVersion = "ORB.VERSION"; + serverTier = "ALL" + } + } + } + + $classifiedPackage = Get-PackageMetadataV2 -Package $hotfixPackage -NugetCredential $fakeCredential -MicroserviceTierPresent + + It "Is going to WEB tier" { + $classifiedPackage.InstallToWebTier | Should -BeTrue + } + + It "Is going to APP tier" { + $classifiedPackage.InstallToAppTier | Should -BeTrue + } + + It "Is going to MIC tier" { + $classifiedPackage.InstallToMicTier | Should -BeTrue + } + + It "Is classified 'HasAlkamiManifest'" { + $classifiedPackage.HasAlkamiManifest | Should -BeTrue + } + + It "Is classified 'IsHotfix'" { + $classifiedPackage.IsHotfix | Should -BeTrue + } + + } + + +} + +Describe "Json Comparison Tests" { + + $packageText = @" +# Installers +Alkami.MicroServices.Choco.Installer.Database 2.4.6 +Alkami.MicroServices.Choco.Installer.Logic 2.4.6 +Alkami.MicroServices.Choco.Installer.MasterDatabase 2.4.6 +Alkami.Installer.Provider 3.0.6 +Alkami.Installer.WebExtension 3.0.1 +Alkami.Installer.Widget 3.0.2 + +# Infrastructure +Alkami.MicroServices.Broker.Host 2.8.1 +Alkami.Services.Subscriptions.Host 3.9.0 + +# Providers +Alkami.App.Nag.Providers 1.1.4 +Alkami.App.Processor.Wire.FedwireOutput 1.6.0 +Alkami.App.Providers.CheckImaging.CorporateOne 1.0.1 +Alkami.App.Providers.Multiplexer.Client 1.0.4 +Alkami.App.Providers.Radium.BillPay 1.1.0 + +# Tier 1 Microservices +Alkami.MicroServices.Forms.Service.Host 1.1.1 +Alkami.MicroServices.Authorization.Service.Host 1.4.3 +Alkami.MicroServices.Security.Service.Host 2.17.2 +Alkami.MicroServices.UserInterface.Service.Host 1.17.0 +Alkami.MicroServices.Audit.Service.Host 6.11.0 +Alkami.MicroServices.Contacts.Service.Host 2.4.8 +Alkami.MicroServices.Holidays.Service.Host 2.1.12 +Alkami.MicroServices.Images.Service.Host 3.1.14 +Alkami.MicroServices.Notifications.Service.Host 1.6.12 +Alkami.MicroServices.Settings.Service.Host 4.4.0 +Alkami.MicroServices.SiteText.Service.Host 1.1.17 +Alkami.MicroServices.Transactions.Service.Host 1.7.1 +Alkami.MicroServices.EventManagement.Service.Host 3.4.9 + +# Tier 2 Microservices +Alkami.MicroServices.QuickApply.Service.Host 9.0.0 +Alkami.MicroServices.Registration.Service.Host 2.5.1 +Alkami.MicroServices.RemoteDeposit.Service.Host 2.4.1 +Alkami.MicroServices.CardManagementProviders.SymConnect.Host 3.7.1 + +# Powershell Modules +Alkami.Ops.Common 3.0.3 +Alkami.PowerShell.Choco 3.5.4 +Alkami.PowerShell.Common 3.2.6 + +# SDK +DS.AccountService.MS.Service.Host 1.0.9 +DS.BranchService.MS.Service.Host 1.0.10 +DS.CMN.MS.Service.Host 1.0.8 +DS.CopyRequestService.MS.Host 1.0.1 +DS.CoreService.MS.Service.Host 2.0.5 +DS.EStatements.MS.Service.Host 1.0.7 +DS.MarketingEmailService.MS.Host 1.0.7 +DS.MicroService.ContentService.Service.Host 2.0.4 +DS.SecureMessage.MS.Service.Host 1.0.10 +DS.Settings.MS.Service.Host 1.0.8 +DS.SkipAPayService.MS.Host 1.0.8 + +# Symitar +Alkami.MicroServices.AutoBiller.Symitar.Service.Host 2.1.4 +Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host 1.1.3 +Alkami.MicroServices.SymConnectMultiplexer.Service.Host 2.1.2 +Alkami.Providers.FakeSymitarProvider 1.2.3 +"@; + + # We get the tags from the nuspec which comes from proget. Mocking fake values... + Mock Get-PackageNuspecXmlV2 -ModuleName $moduleForMock { + param($Package, $Credential) + $name = $Package.Name + return (New-Object PSObject -Property @{ + package = @{ + metadata = @{ id = $name + tags = @( + "FakeTag", + "OtherFakeTag" + ) + } + } + }) + } + + Mock -CommandName Get-PackageFileV2 -ModuleName $moduleForMock -MockWith {} + + # Strip out the comment lines and parse the packages out into package objects. + $packageText = ($packageText -split "`r`n") | Where-Object { $_ -notlike "*#*"} + $packages = Format-ParseChocoPackages -text $packageText -delimiter " " + $fakeCredential = New-Object System.Management.Automation.PSCredential('AlkamiFakeUser', (Get-SecureString 'abc123')) + + # Attach fake source feeds onto each package, since we don't want to run this test against real nuget feeds. + foreach($package in $packages) + { + $feedMap = @{ + Disabled = $false + IsDefault = $false + IsSdk = $package.Name -like "DS.*" # Mark the desert schools packages as SDK. + Name = if ($package.Name -like "DS.*") {"SDK"} Else {"Alkami"} + Priority = "0" + Source = "https://packagerepo.orb.alkamitech.com/nuget/fake.feed" + } + $package.Feed = New-Object PSObject -Property $feedMap + } + # Run classification function on all of the packages. + + Context "When Generating Packages"{ + # Create Test cases. See Get-ChocolateyParameterStrings tests. + $testCases = @() + foreach ($package in $packages){ + $testCases += @{ + caseDescription = 'Matches Pre-Defined Files' + package = $package + testName = $package.Name + environment = '' + } + } + + Mock -CommandName Write-Host -MockWith {} + + It " for " -TestCases $testCases { + param( + $package, + $environment + ) + + # this long magic string is a barebones package folder with no useful contents. Just the basic nuget structure and a choco install file set + Mock Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith {return (ConvertFrom-Json ('[{{"fullPath":"package/services/metadata/core-properties/","parentFullPath":"package/services/metadata/","name":"core-properties","isDirectory":true}},{{"fullPath":"package/services/metadata/","parentFullPath":"package/services/","name":"metadata","isDirectory":true}},{{"fullPath":"package/services/","parentFullPath":"package/","name":"services","isDirectory":true}},{{"fullPath":"package/","parentFullPath":"/","name":"package","isDirectory":true}},{{"fullPath":"package/services/metadata/core-properties/629d5521ad934906a0d3c36051d35320.psmdcp","parentFullPath":"package/services/metadata/core-properties/","name":"629d5521ad934906a0d3c36051d35320.psmdcp","isDirectory":false}},{{"fullPath":"lib/net472/","parentFullPath":"lib/","name":"net472","isDirectory":true}},{{"fullPath":"lib/","parentFullPath":"/","name":"lib","isDirectory":true}},{{"fullPath":"_rels/","parentFullPath":"/","name":"_rels","isDirectory":true}},{{"fullPath":"_rels/.rels","parentFullPath":"_rels/","name":".rels","isDirectory":false}},{{"fullPath":"tools/","parentFullPath":"/","name":"tools","isDirectory":true}},{{"fullPath":"tools/chocolateyInstall.ps1","parentFullPath":"tools/","name":"chocolateyInstall.ps1","isDirectory":false}},{{"fullPath":"tools/chocolateyUninstall.ps1","parentFullPath":"tools/","name":"chocolateyUninstall.ps1","isDirectory":false}},{{"fullPath":"[Content_Types].xml","parentFullPath":"/","name":"[Content_Types].xml","isDirectory":false}},{{"fullPath": "{0}.exe.config","parentFullPath": "/","name": "{0}.exe.config","isDirectory": false}}]' -f $package.Name))}.GetNewClosure() + + # NR AppName comes from a nuspec. We're arbitrarily deciding that microservices have one and others don't for our tests. + Mock Get-AppSetting -ModuleName $moduleForMock -MockWith { + # Microservices have a NR app name + if(($package.Name -like "*.MicroService*.*") -or ($package.Name -like "*.MS.*")){ + return "FakeNRAppName" + } + # Other packages have no NR app name. + else { + return $null + } + }.GetNewClosure() + + $packageData = Get-PackageMetadataV2 -Package $package -NugetCredential $fakeCredential + + $packageJson = ConvertTo-Json $packageData + + # Find matching file in .\\TestFiles\PackageObjects\ + $jsonFiles = Get-ChildItem -Path $PSScriptRoot\..\TestFiles\PackageObjects + foreach ($file in $jsonFiles) { + if ($file.Name -like "$($package.Name).json" ) { + $fileContents = Get-Content $file.FullName + # Sort the Json Alphabetically + + Write-OrderedJson -JsonObject $fileContents -Path "TestDrive:\expected.json" + Write-OrderedJson -JsonObject $packageJson -Path "TestDrive:\results.json" + + $expectedContent = Get-Content "TestDrive:\expected.json" + $actualContent = Get-Content "TestDrive:\results.json" + + # Debugging a single package can be frustrating without being able to look at it specifically. + # Specifying a specific package name here will run the test for a single package, and give 2 files that you can examine in beyond compare. + # To do so, uncomment this if block and the Copy-Item lines. (You'll get automatic "successes" for other packages as they would no longer run the assert step) + + #if($package.Name -eq "Alkami.Installer.Widget"){ + #Copy-Item "TestDrive:\Expected.json" ".\Expected.Json" + #Copy-Item "TestDrive:\results.json" ".\results.Json" + # Assert equality. + $actualContent | Should -Be $expectedContent + #} + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageNuspecXmlV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageNuspecXmlV2.ps1 new file mode 100644 index 0000000..08ac58e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageNuspecXmlV2.ps1 @@ -0,0 +1,78 @@ +function Get-PackageNuspecXmlV2 { +<# +.SYNOPSIS + Returns the .nuspec XML object for the given package. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER Package + [Object] Known package object with properties as { Feed={ Source=; Name=; } Name=; Version=; } + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName='RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + ## TODO: Can this pull from the local filesystem if it exists? + ## This would let us fetch faster if the versions match as we could avoid network hops. + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $FeedSource = $Package.Feed.Source + $Name = $Package.Name + $Version = $Package.Version + } + + $splatVar = @{ + FeedSource = $FeedSource + Name = $Name + Version = $Version + Credential = $Credential + + PackagePath = "$Name.nuspec" + } + + # See if the nuspec is cached in the session. + $existsInCache = ($Script:GetPackageNuspecXmlCacheName -eq $Name) -and + ($Script:GetPackageNuspecXmlCacheSource -eq $FeedSource) -and + ($Script:GetPackageNuspecXmlCacheVersion -eq $Version) + + if($existsInCache -and ($null -ne $Script:GetPackageNuspecXmlCacheNuspec)) { + Write-Verbose "$loglead : Nuspec for $name|$version is cached. Returning cached version." + return $Script:GetPackageNuspecXmlCacheNuspec + } + + # Query for the nuspec from Proget. + Write-Verbose "$loglead : Nuspec for $name|$version is not cached. Querying for nuspec from proget." + [xml]$result = [xml](Get-PackageFile @splatVar) + + # Cache the nuspec. + $Script:GetPackageNuspecXmlCacheName = $Name + $Script:GetPackageNuspecXmlCacheSource = $FeedSource + $Script:GetPackageNuspecXmlCacheVersion = $Version + $Script:GetPackageNuspecXmlCacheNuspec = $result + + return $result +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.ps1 new file mode 100644 index 0000000..c8a7322 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.ps1 @@ -0,0 +1,226 @@ +function Get-PackageVersionsFromAlkamiProget { +<# +.SYNOPSIS + This script searches for packages in the dev and qa ProGet feeds. If found, returns a JSON representation of package names and versions to the user. + +.DESCRIPTION + This script searches for packages in the dev and qa ProGet feeds. Accepted input is the full path to a text file containing + one package name per line (PackageNamesFile), or a comma-delimited string of full or partial package names (PackageNames). + If the search for a given package name returns results, they are added to a hash table. After all input package names are + searched for, the hash table is converted to JSON and displayed to the user. + +.PARAMETER DevUrl + An optional string that allows the user to specify a Dev proget URL to search. Defaults to Alkami's current dev feed. + +.PARAMETER PackageNames + An optional string containing the names of the packages to search for. Comma-delimited for multiple packages. Partial names are + acceptable if exact matching isn't specified. Results will vary if only partial names are provided. + +.PARAMETER PackageNamesFile + An optional flag expecting the full path to a file. Searches for all package names in the given file. Expected file contents is + one line per package name in the file. + +.PARAMETER QaUrl + An optional string that allows the user to specify a QA proget URL to search. Defaults to Alkami's current QA feed. + +.PARAMETER ExactMatch + An optional flag that searches for the exact name of the provided packages. If not passed, searches will return partial matches. + +.PARAMETER SearchDevFeed + An optional flag that tells the script to search the dev feed for packages. + +.PARAMETER SearchQaFeed + An optional flag that tells the script to search the qa feed for packages. If neither of the feed-specific parameters are + passed, defaults to dev. + +.PARAMETER ShowPreVersions + An optional flag that tells the script to also search the dev feed for pre versions of the specified packages. + +.EXAMPLE + Get-PackageVersionsFromAlkamiProget -PackageNames "Alkami.Apps.Benefits,Alkami.Apps.AugeoRewards,Alkami.MS.SSOProviders.AugeoRewards.Host" -ExactMatch -ShowPreVersions + +Search just the dev feed for release and pre versions of packages by exact name + +.EXAMPLE + Get-PackageVersionsFromAlkamiProget -PackageNames "overdraft,orbital,rules" -SearchDevFeed -SearchQaFeed -ShowPreVersions + +Search the qa and dev feeds for release and pre versions of packages by partial names. NOTE: these results can vary. The chocolatey search does not just search on package name. Tags and other fields in ProGet can cause a package to be returned. + +.EXAMPLE + Get-PackageVersionsFromAlkamiProget -PackageNamesFile "C:\Temp\testFile.txt" + +Search the dev feed for release versions of packages in a given package names file. NOTE: The file format the script expects is a text file with one package name per line. + +#> + + Param( + [CmdletBinding()] + [Parameter(Mandatory = $false)] + [string] + $DevUrl = "https://packagerepo.orb.alkamitech.com/nuget/choco.dev", + + [Parameter(Mandatory = $false)] + [string] + $PackageNames = "", + + [Parameter(Mandatory = $false)] + [string] + $PackageNamesFile = "", + + [Parameter(Mandatory = $false)] + [string] + $QaUrl = "https://packagerepo.orb.alkamitech.com/nuget/choco.qa", + + [Parameter(Mandatory = $false)] + [Switch] + $ExactMatch, + + [Parameter(Mandatory = $false)] + [Switch] + $SearchDevFeed, + + [Parameter(Mandatory = $false)] + [Switch] + $SearchQaFeed, + + [Parameter(Mandatory = $false)] + [Switch] + $ShowPreVersions + ) + + $logLead = (Get-LogLeadName) + + $feedsArray = @() + $resultsHash = [ordered]@{} + + # Parameter validation + if ($PSBoundParameters.ContainsKey('PackageNames')) { + if ([string]::IsNullOrEmpty($PackageNames)) { + Throw "PackageNames was provided but it is null or empty. Please specify at least one value to search for. The script expects a comma-delimited string of full or partial package names to search for." + } + if ($PSBoundParameters.ContainsKey('PackageNamesFile')) { + Throw "Both the PackageNames and PackageNamesFile parameters were passed. Please only specify one or the other." + } + Write-Host "`n$logLead : Package names supplied by user. Package names to search for are: $packageNames" + } + + if (!$PSBoundParameters.ContainsKey('PackageNames')) { + if (!$PSBoundParameters.ContainsKey('PackageNamesFile')) { + Throw "Neither the PackageNames parameter nor the PackageNamesFile parameter were provided. Please use one or the other to specify packages to search for." + } + } + + if ($PSBoundParameters.ContainsKey('PackageNamesFile')) { + if ([string]::IsNullOrEmpty($PackageNamesFile)) { + Throw "The PackageNamesFile parameter was provided but it is empty or null. Please specify the full filepath to a text file containing a list of packagenames, one package per line. Make sure to provide the full path to the file with file extension. i.e. 'C:\Temp\test.txt'" + } + + if (Test-Path -Path $PackageNamesFile) { + Write-Host "`n$logLead : Package names file directory specified by the user. Searching for packages found in file: '$PackageNamesFile'" + } else { + Throw "Path: '$PackageNamesFile' does not exist. Make sure to provide the full path to the file with file extension. i.e. 'C:\Temp\test.txt'" + } + + $fileContentsArray = Get-Content $PackageNamesFile + if (Test-IsCollectionNullOrEmpty $fileContentsArray.Length) { + Throw "A package names file was specified, but the file is empty. Please double-check the directory or file provided." + } else { + Write-Host "$logLead : Number of package names found in the file: $($fileContentsArray.Length)" + $packageNames = $fileContentsArray -join "," + } + } + + if ($SearchDevFeed.IsPresent) { + Write-Host "$logLead : SearchDevFeed parameter was passed by the user. The dev feed will be searched." + $feedsArray += "dev" + } + + if ($SearchQaFeed.IsPresent) { + Write-Host "$logLead : SearchQaFeed parameter was passed by the user. The qa feed will be searched." + $feedsArray += "qa" + } + + if (Test-IsCollectionNullOrEmpty $feedsArray) { + Write-Host "$logLead : Neither SearchDevFeed nor SearchQaFeed were passed by the user. The dev feed will be searched by default." + $feedsArray += "dev" + } + + if ($ShowPreVersions.IsPresent) { + Write-Host "$logLead : The ShowPreVersions parameter was passed. This script will also search for pre versions of the provided packages." + } + + if ($ExactMatch.IsPresent) { + Write-Host "$logLead : ExactMatch parameter was passed. Searches will use exact name matching." + } + + $packageNamesArray = $packageNames.Split(","); + $packageNamesArray = $packageNamesArray | Select-Object -Unique + + foreach ($packageName in $packageNamesArray) { + $packageName = $packageName.Trim() + foreach ($feed in $feedsArray) { + if ($feed -eq "dev") { + Write-Host "`n$logLead : Searching feed: $feed with search filter: '$packageName'" + $temp = Get-ChocoState -s "$DevUrl" -packageName "$packageName" -exact:$ExactMatch + if ($NULL -ne $temp) { + foreach ($result in $temp) { + Write-Verbose "$logLead : Current result is: '$result'" + if ($resultsHash.Keys -contains "$($result.Name)") { + if ($resultsHash[$($result.Name)].Keys -contains "DevRelease") { + Write-Verbose "$logLead : Results already contain a DevRelease entry for package '$($result.Name)'. Skipping add." + } else { + $resultsHash[$result.Name] += @{"DevRelease" = $result.Version } + } + } else { + $resultsHash[$result.Name] += @{"DevRelease" = $result.Version } + } + } + } + if ($ShowPreVersions) { + Write-Host "`n$logLead : Searching feed: $feed for pre versions with search filter: '$packageName'" + $temp = Get-ChocoState -s "$DevUrl" -packageName "$packageName" -exact:$ExactMatch -pre + if ($NULL -ne $temp) { + foreach ($result in $temp) { + Write-Verbose "$logLead : Current result is: '$result'" + if ($result.Version -like "*pre*") { + if ($resultsHash.Keys -contains "$($result.Name)") { + if ($resultsHash[$($result.Name)].Keys -contains "DevPre") { + Write-Verbose "$logLead : Results already contain a DevPre entry for package '$($result.Name)'. Skipping add." + } else { + $resultsHash[$result.Name] += @{"DevPre" = $result.Version } + } + } else { + $resultsHash[$result.Name] += @{"DevPre" = $result.Version } + } + } + } + } + } + } + if ($feed -eq "qa") { + Write-Host "`n$logLead : Searching feed: $feed with search filter: '$packageName'" + $temp = Get-ChocoState -s "$QaUrl" -packageName "$packageName" -exact:$ExactMatch + if ($NULL -ne $temp) { + foreach ($result in $temp) { + Write-Verbose "$logLead : Current result is: '$result'" + if ($resultsHash.Keys -contains "$($result.Name)") { + if ($resultsHash[$($result.Name)].Keys -contains "QaRelease") { + Write-Verbose "$logLead : Results already contain a QaRelease entry for package '$($result.Name)'. Skipping add." + } else { + $resultsHash[$result.Name] += @{"QaRelease" = $result.Version } + } + } else { + $resultsHash[$result.Name] += @{"QaRelease" = $result.Version } + } + } + } + } + } + } + Write-Host "" + if ($resultsHash.Keys.Count -eq 0) { + Write-Warning "$logLead : No results were found given the following:`nDevUrl: '$DevUrl'`nQaUrl: '$QaUrl'`n`nPackages names searched for:`n$packageNamesArray" + } + $resultsJson = $resultsHash | ConvertTo-Json + return $resultsJson +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.tests.ps1 new file mode 100644 index 0000000..1b89ff6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-PackageVersionsFromAlkamiProget.tests.ps1 @@ -0,0 +1,172 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe Get-PackageVersionsFromAlkamiProget { + $samplePackageFileDirectory = "C:\test\test.txt" + $samplePackageNames = "Alkami.Test.Microservice" + + Mock Get-Content -ModuleName $moduleForMock { + return @() + } + + Context "Parameter validation" { + + Mock Write-Warning -ModuleName $moduleForMock { } + + It "Throw when Both PackageNames Sources Passed" { + { Get-PackageVersionsFromAlkamiProget -PackageNamesFile $samplePackageFileDirectory -PackageNames $samplePackageNames } | Should -Throw + } + + It "Throw when PackageNames is provided but empty" { + { Get-PackageVersionsFromAlkamiProget -PackageNames "" } | Should -Throw + } + + It "Throw when the file from PackageNamesFile is empty" { + Mock Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock { + return $true + } + Mock Write-Host -ModuleName $moduleForMock { } + + { Get-PackageVersionsFromAlkamiProget -PackageNamesFile $samplePackageFileDirectory } | Should -Throw + } + + It "Throw when the file from PackageNamesFile doesn't exist" { + { Get-PackageVersionsFromAlkamiProget -PackageNamesFile $samplePackageFileDirectory } | Should -Throw + } + + It "Throw when the string for PackageNamesFile is empty" { + { Get-PackageVersionsFromAlkamiProget -PackageNamesFile "" } | Should -Throw + } + + It "Throw when neither PackageNames nor PackageNamesFile parameters are provided" { + { Get-PackageVersionsFromAlkamiProget -SearchDevFeed } | Should -Throw + } + + It "Correct amount of write-hosts called when PackageNames and all switch parameters are passed" { + Mock Write-Host -ModuleName $moduleForMock { } + + Get-PackageVersionsFromAlkamiProget -PackageNames $samplePackageNames -ExactMatch -ShowPreVersions -SearchDevFeed -SearchQaFeed + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Scope It -Exactly 12 + } + + It "Correct amount of write-hosts called when only PackageNamesFile parameter is passed" { + Mock Get-Content -ModuleName $moduleForMock { + return @() + } + Mock Test-IsCollectionNullOrEmpty -ModuleName $moduleForMock { + return $false + } + Mock Test-Path -ModuleName $moduleForMock { + return $true + } + + Get-PackageVersionsFromAlkamiProget -packageNamesFile $samplePackageFileDirectory + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Scope It -Exactly 3 + } + + It "Correct amount of write-hosts called when SearchQaFeed parameter is passed and SearchDevFeed is not" { + Mock Write-Host -ModuleName $moduleForMock { } + + Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchQaFeed + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Scope It -Exactly 4 + } + + It "Correct amount of write-hosts called when neither SearchDevFeed nor SearchQaFeed is passed" { + Mock Write-Host -ModuleName $moduleForMock { } + + Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -Scope It -Exactly 2 + } + } + + Context "Output validation" { + Mock Get-ChocoState -ModuleName $moduleForMock { + $results = @(@{ Feed = $NULL; IsService = $NULL; Version = "1.0.1"; Tags = $NULL; Name = "Alkami.Test.Test1" }, + @{ Feed = $NULL; IsService = $NULL; Version = "1.1.0"; Tags = $NULL; Name = "Alkami.Test.Test2" }, + @{ Feed = $NULL; IsService = $NULL; Version = "1.2.0-pre099"; Tags = $NULL; Name = "Alkami.TestPre.Test" }) + return $results + } + + Mock Write-Host -ModuleName $moduleForMock { } + + It "DevRelease versions of packages are displayed by default when no feed switches are provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames + $results | Should -BeLike "*DevRelease*" + } + + It "DevRelease versions of packages are displayed when SearchDevFeed is provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchDevFeed + $results | Should -BeLike "*DevRelease*" + } + + It "DevPre versions of packages are displayed when ShowPreVersions is provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -ShowPreVersions + $results | Should -BeLike "*DevPre*" + } + + It "QaRelease versions of packages are displayed when SearchQaFeed is provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchQaFeed + $results | Should -BeLike "*QaRelease*" + } + + It "DevRelease, DevPre, and QaRelease versions are displayed when SearchDevFeed, ShowPreVersions, and SearchQaFeed are provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchQaFeed -ShowPreVersions -SearchDevFeed + $results | Should -BeLike "*QaRelease*" + $results | Should -BeLike "*DevPre*" + $results | Should -BeLike "*DevRelease*" + } + + It "DevRelease versions are not displayed when SearchDevFeed is not passed, and SearchQaFeed or ShowPreVersions is passed" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchQaFeed -ShowPreVersions + $results | Should -Not -BeLike "*DevRelease*" + } + + It "QaRelease versions are not displayed when the SearchQaFeed parameter is not provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchDevFeed -ShowPreVersions + $results | Should -Not -BeLike "*QaRelease*" + } + + It "DevPre versions are not displayed when the SearchDevFeed parameter is not provided" { + $results = Get-PackageVersionsFromAlkamiProget -packageNames $samplePackageNames -SearchDevFeed -SearchQaFeed + $results | Should -Not -BeLike "*DevPre*" + } + + It "Returns an empty JSON string and warns the user if no results are found" { + Mock Get-ChocoState -ModuleName $moduleForMock { + $results = $NULL + return $results + } + Mock Write-Warning -ModuleName $moduleForMock { } + + $results = Get-PackageVersionsFromAlkamiProget -packageNames "doesntexist" + $emptyHashToJson = @{} | ConvertTo-Json + $results | Should -BeExactly $emptyHashToJson + + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Scope It -Exactly 1 + } + + It "Hash duplicate checks make function return only one instance of a package when the package is found by get-chocostate multiple times" { + Mock Get-ChocoState -ModuleName $moduleForMock { + $results = @(@{ Feed = $NULL; IsService = $NULL; Version = "1.0.1-pre000"; Tags = $NULL; Name = "Alkami.Test" }) + return $results + } + Mock Write-Verbose -ModuleName $moduleForMock { } + + $results = Get-PackageVersionsFromAlkamiProget -packageNames "testIterationOne,testIterationTwo" -SearchDevFeed -SearchQaFeed -ShowPreVersions -Verbose + + Assert-MockCalled -CommandName Write-Verbose -ModuleName $moduleForMock -Scope It -ParameterFilter {$Message -like "*Results already contain a DevRelease entry*"} + Assert-MockCalled -CommandName Write-Verbose -ModuleName $moduleForMock -Scope It -ParameterFilter {$Message -like "*Results already contain a DevPre entry*"} + Assert-MockCalled -CommandName Write-Verbose -ModuleName $moduleForMock -Scope It -ParameterFilter {$Message -like "*Results already contain a QaRelease entry*"} + + $convertedResults = $results | ConvertFrom-Json + $convertedResults.'Alkami.Test'.DevRelease | Should -Be "1.0.1-pre000" + $convertedResultsCount = ($convertedResults | Get-Member -Type NoteProperty).count + $convertedResultsCount | Should -Be 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-RemoteInstalledChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-RemoteInstalledChocoPackages.ps1 new file mode 100644 index 0000000..88eaaf9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-RemoteInstalledChocoPackages.ps1 @@ -0,0 +1,18 @@ +function Get-RemoteInstalledChocoPackages { + <# +.SYNOPSIS + Get packages installed on remote servers +#> + [CmdletBinding()] + param ( + [array]$ServersToQuery + ) + + $scriptBlock = { + $result = @{ } + $result[([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname] = choco.exe list -l -r + return $result + } + + return Invoke-Command -ComputerName $ServersToQuery -ScriptBlock $scriptBlock -WarningAction silentlyContinue +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ServicesByTier.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ServicesByTier.ps1 new file mode 100644 index 0000000..14021f9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ServicesByTier.ps1 @@ -0,0 +1,45 @@ +function Get-ServicesByTier { +<# +.SYNOPSIS + Returns service names belonging to the specified tier and optionally lower + +.PARAMETER Tier + [-Tier] - Returns a containing names of tiered services categorized + under the provided numbered tier. + +.PARAMETER IncludeLowerTiers + [-IncludeLowerTiers] - Also include services from all tiers lower than the provided tier. + (With this swtich present, -Tier 0 returns only tier 0 service names, 1 returns both tier 0 and 1 services, 2 returns + tier 2, 1, and 0, ad infinitum.) Values provided above the number of defined tiers simply returns all available tiered services. +#> + [CmdletBinding()] + [OutputType([System.Object])] + param( + [Parameter(Mandatory = $true)] + [ValidateRange(0, [int]::MaxValue)] + [int]$Tier, + + [Parameter(Mandatory = $false)] + [switch]$IncludeLowerTiers + ) + + $tiers = Get-MicroserviceTiers + + #Only return a single tier if switch is not present + if (!($IncludeLowerTiers.IsPresent)) { + return $tiers[$Tier] + } + + $currentTier = 0 + $services = @() + foreach ($servicesArray in $tiers) { + if ($currentTier++ -le $Tier) { #Tip: ++ happens _after_ evaluation when it's on the right + $services += $servicesArray + } + else { + break + } + } + + return $services +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Get-ValidHotfixServerTiers.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Get-ValidHotfixServerTiers.ps1 new file mode 100644 index 0000000..e76c9d1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Get-ValidHotfixServerTiers.ps1 @@ -0,0 +1,15 @@ +function Get-ValidHotfixServerTiers { + <# +.SYNOPSIS + Returns an array of valid Server Tiers(types) to be used in Hotfix manifests as a target +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + [array]$validHotfixServerTiers = @( + "APP", + "WEB", + "ALL" + ) + return $validHotfixServerTiers +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Install-ExistingMicroservicesWithMigrations.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Install-ExistingMicroservicesWithMigrations.ps1 new file mode 100644 index 0000000..f49bf04 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Install-ExistingMicroservicesWithMigrations.ps1 @@ -0,0 +1,19 @@ +function Install-ExistingMicroservicesWithMigrations { +<# +.SYNOPSIS + Force Re-Installs microservices which may have migrations. +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $msToInstall = Get-MicroservicesWithMigrations + + foreach ($ms in $msToInstall) { + Write-Host ("$logLead : Force Installing {0} v{1}" -f $ms.Name, $ms.Version) + choco install $ms.Name --version $ms.Version -f -y; + } +} + + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Install-LegacyUtilityPackage.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Install-LegacyUtilityPackage.ps1 new file mode 100644 index 0000000..a233b09 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Install-LegacyUtilityPackage.ps1 @@ -0,0 +1,66 @@ +function Install-LegacyUtilityPackage { +<# +.SYNOPSIS + Used to install a legacy Alkami utility. + Examples would include the legacy "Deconversion" utility, the CacheBuster or LogParser tools, etc, from the ORB legacy product. + +.PARAMETER Path + [string] The location where the files are already installed to. (Typically via package manager, such as chocolatey) + +.PARAMETER NeedsShared + [switch] Used to indicate this project uses the legacy Alkami.Ioc.dll library that requires access to the C:\Orb\Shared or similar folder. +#> + [CmdletBinding()] + [OutputType([void])] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Path, + [switch]$NeedsShared + ) + + $loglead = (Get-LogLeadName) + + if (!(Test-IsAdmin)){ + throw "$logLead : You are not running as administrator. Can not continue." + } + + # Why continue if the files don't exist on disk? + if (!(Test-Path $Path)) { + throw "$logLead : Could not find the path specified at [$Path]" + } + + if (!(Test-PathIsInApprovedPackageLocation $Path)) { + Write-Error "$logLead : Can not configure legacy utilities from paths not in 'Approved Locations' (see Test-PathIsInApprovedPackageLocation) using this installer." + return + } + + $utilityName = (Get-ChocoPackageFromPath -Path $Path) + + Write-Host "$loglead : LegacyUtility [$utilityName] being installed by $($env:username) on $($env:computername) at $(Get-Date)" + + if ($NeedsShared) { + ## This is in a choco or similar location. + ## We should take the parent of our path and ensure that a "Shared" symlink exists at that point + ## The purpose of this is for Alkami.Ioc resolver to find the parent path in the lookup + ## This introduces a hard-limit that no package can be called Shared (unless it's the Legacy ORB "Shared" folder from the build process) + $SharedTargetPath = (Join-Path (Split-Path $Path -Parent) "Shared") + + if (!(Test-Path $SharedTargetPath)) { + <# + ## Create the symlink here + cmd /c mklink /J $SharedTargetPath (Get-OrbSharedPath) + ## can be rewritten as + $cmdArguments @( + "/c" # run a command as provided in the following strings + "mklink" # run the make-link command from the cmd.exe interpreter + "/J" # create a junction type + $SharedTargetPath # where we are targeting + (Get-OrbSharedPath) # where it should actually point to + ) + (Invoke-CallOperatorWithPathAndParameters "C:\WINDOWS\System32\cmd.exe" $cmdArguments) + ## which is better represented by the following line + #> + (New-Item -ItemType SymbolicLink -Path $SharedTargetPath -Target (Get-OrbSharedPath)) | Out-Null + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Install-ManualChocoPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Install-ManualChocoPackages.ps1 new file mode 100644 index 0000000..3601f22 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Install-ManualChocoPackages.ps1 @@ -0,0 +1,32 @@ +function Install-ManualChocoPackages { +<# +.SYNOPSIS + Runs a chocolatey install script for packages that must be installed manually. + For now this includes new packages, and packages that have database migrations. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [string]$commandsPath = "C:/temp/deploy/chocoInstallCommands.ps1" + ) + + $logLead = (Get-LogLeadName); + if(!(Test-Path $commandsPath)) + { + Write-Warning "$logLead Chocolatey install script $commandsPath was not found." + } + else + { + Write-Host "$logLead Executing install script at $commandsPath" + & $commandsPath + + Write-Host "$logLead Install script has run successfully. Backing up choco commands file." + $fileInfo = Get-ChildItem $commandsPath + $newPath = Join-Path $fileInfo.DirectoryName ("backup-" + $fileInfo.Name) + + Write-Host "$logLead Saving backup choco command file to $newPath" + Move-Item -Path $commandsPath -Destination $newPath -force + Write-Host "$logLead Backup complete." + } +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Install-MicroservicesWithMigrations.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Install-MicroservicesWithMigrations.ps1 new file mode 100644 index 0000000..d135a62 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Install-MicroservicesWithMigrations.ps1 @@ -0,0 +1,16 @@ +function Install-MicroservicesWithMigrations { +<# +.SYNOPSIS + Note: Alias of Install-ManualChocoPackages. + This function name was changed because it was a misnomer. + Install-ManualChocoPackages installs both packages with migrations, AND new choco packages. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [string]$commandsPath = "C:/temp/deploy/chocoInstallCommands.ps1" + ) + + Install-ManualChocoPackages $commandsPath +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackages.ps1 new file mode 100644 index 0000000..27406a9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackages.ps1 @@ -0,0 +1,31 @@ +function Invoke-ChocoInstallPackages { +<# +.SYNOPSIS + Installs a list of Chocolatey packages. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("Packages")] + [string]$packagesText, + + [Parameter(Mandatory = $false)] + [Alias("f")] + [switch]$force, + + [Parameter(Mandatory = $false)] + [Alias("e")] + [string]$environment + ) + + $packageObjects = Get-ChocoStateReleaseInput($packagesText); + + $params = @{ + "packages" = $packageObjects; + "force" = $force.IsPresent; + "e" = $environment; + }; + + Invoke-ChocoInstallPackagesPrivate @params; +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.ps1 new file mode 100644 index 0000000..d4bfe19 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.ps1 @@ -0,0 +1,158 @@ +function Invoke-ChocoInstallPackagesPrivate { +<# +.SYNOPSIS + Installs a list of Chocolatey packages. +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Alkami generates this string manually, no user injection')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("PackagesToInstall")] + [object[]]$packages, + + [Parameter(Mandatory = $false)] + [Alias("f")] + [switch]$force, + + [Parameter(Mandatory = $false)] + [Alias("e")] + [string]$environment, + + [Parameter(Mandatory = $false)] + [Alias("migrate", "m")] + [switch]$runMigrations, + + [Parameter(Mandatory = $false)] + [Alias("start", "s")] + [switch]$startService, + + [Parameter(Mandatory = $false)] + [switch]$forceSameVersion, + + [Parameter(Mandatory = $false)] + [switch]$ignoreDependencies, + + [Parameter(Mandatory = $false)] + [switch]$preventDowngrade, + + [Parameter(Mandatory = $false)] + [switch]$SkipScripts + ) + + $loglead = Get-LogLeadName; + + if (!(Test-IsAdmin)) { + throw "$loglead : User must be an administrator to run Chocolatey installs." + } + + if($packages.Count -gt 1) { + $localPackages = Get-ChocoState -l; + } else { + $localPackages = Get-ChocoState -l -packageName $packages[0].Name; + } + + $numPackages = $packages.count; + Write-Verbose "$loglead : Now installing $numPackages packages:"; + + $success = $true; + $counter = 0; + foreach ($package in $packages) { + $name = $package.Name; + $version = $package.Version; + $counter++ + + try { + $chocoParams = Get-ChocolateyParameterString $package -env $environment -start $startService.IsPresent -migrate $runMigrations.IsPresent; + $localPackage = ($localPackages | Where-Object { $_.Name -eq $name }); + $isInstalled = $null -ne ($localPackage); + + # Upgrade if the package is already installed. + # set local package version to null if not installed + if (!$isInstalled ) { + $commandFlag = "install" + $localPackageVersion = "0.0.0" + } else { + $commandFlag = "upgrade" + $localPackageVersion = $localpackage.Version + } + # Compare two sematic verions + # -1 if $Version1 < $Version2 + # 0 if $Version1 = $Version2 + # 1 if $Version1 > $Version2 + + switch ((Compare-SemVer -version1 $localPackageVersion -version2 $version)) { + 1 { + $isSameVersion = $false + $isDowngrade = $true + } + 0 { + # The Compare-SemVer function will say that certain pre-versions are the same version, and prevent installs. + # This string version comparison hack allows these packages to be considered different versions. + $isSameVersion = $localPackageVersion -eq $version + } + -1 { + $isSameVersion = $false + $isDowngrade = $false + } + Default { + + } + } + + $forceSwitch = ""; + $forceInstall = $force.IsPresent -or ($forceSameVersion.IsPresent -and $isSameVersion); + if ($forceInstall) { + $forceSwitch = "-f"; + } + + # If we're not forcing a package, and it's the same version, then skip the choco install. + if((!$forceInstall) -and ($isInstalled) -and ($isSameVersion)) { + Write-Host "$loglead : Skipping install of $name $version because it is already installed." + continue; + } + + # Handle ignore-dependency / allow-downgrade flags + $ignoreDependenciesFlag = if ($ignoreDependencies.IsPresent -or $isSameVersion -or $isDowngrade) { + "--ignore-dependencies" + } else { + "" + }; + + $downgradeFlag = if ($preventDowngrade.IsPresent) { + "" + } elseif ($isDowngrade) { + "--allow-downgrade" + } else { + "" + }; + + #Skips running `chocoInstall.ps1` as part of `choco install/upgrade` process + #Still adds requested package to /lib + #Makes choco "think" this package is "installed" without running install script + $skipInstallScriptFlag = if ($SkipScripts.IsPresent) { + "--skipscripts" + } else { + "" + } + + if ($isSameVersion -and $forceInstall -and $skipInstallScriptFlag) { + Write-Host "$loglead : Skipping download to just copy same version with componentized installer." + continue + } + Write-Host "$loglead : Now installing ($counter/$numPackages) '$name $version':"; + + $command = "choco $commandFlag $name --version $version -y --no-progress $downgradeFlag $ignoreDependenciesFlag $forceSwitch $skipInstallScriptFlag --params $chocoParams;" + Write-Host "Command: $command" + Invoke-Expression $command + + Write-Host "$loglead : Installation of '$name $version $skipInstallScriptFlag' done." + } catch { + Write-Error $_ | Format-List -force + $success = $false; + } + } + + if (!($success)) { + throw "$loglead : Installation of one or more packages failed."; + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.tests.ps1 new file mode 100644 index 0000000..f489edc --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ChocoInstallPackagesPrivate.tests.ps1 @@ -0,0 +1,112 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Invoke-ChocoInstallPackagesPrivate" { + Mock Invoke-Expression -ModuleName $moduleForMock { } + + Context "build version" { + BeforeEach { + Mock Get-ChocoState -ModuleName $moduleForMock { return @{Name = "dummyPackage"; Version = "1.3.2" } } + } + + It "null -> 1.3.3 should not contain --allow-downgrade --ignore-dependencies" { + Mock Get-ChocoState -ModuleName $moduleForMock { } + + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.3" } -Verbose + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "1.3.2 -> 1.3.3 should not contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.3" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "1.3.2 -> 1.3.2 should contain--ignore-dependencies; not --allow-downgrade" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.2" } -forceSameVersion + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -like "*--ignore-dependencies*") } + } + + It "1.3.2 -> 1.3.1 should contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.1" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -like "*--allow-downgrade*") -or ($command -like "*--ignore-dependencies*") } + } + } + + Context "pre version" { + BeforeEach { + Mock Get-ChocoState -ModuleName $moduleForMock { return @{Name = "dummyPackage"; Version = "6.6.6-pre666" } } + } + + It "null -> 6.6.6-pre666 should not contain --allow-downgrade --ignore-dependencies" { + Mock Get-ChocoState -ModuleName $moduleForMock { } + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "6.6.6-pre666" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "6.6.6-pre666 -> 6.6.6-pre777 should not contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "6.6.6-pre777" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "6.6.6-pre666 -> 6.6.6-pre666 should contain--ignore-dependencies; not --allow-downgrade " { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "6.6.6-pre666" } -forceSameVersion + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -like "*--ignore-dependencies*") } + } + + It "6.6.6-pre666 -> 6.6.6-pre111 should contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "6.6.6-pre111" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -like "*--allow-downgrade*") -and ($command -like "*--ignore-dependencies*") } + } + } + Context "major minor version" { + BeforeEach { + Mock Get-ChocoState -ModuleName $moduleForMock { return @{Name = "dummyPackage"; Version = "7.7.7" } } + } + + It "7.7.7 -> 8.8.8 should not contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "8.8.8" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "7.7.7 -> 2.2.2 should contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "2.2.2" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -like "*--allow-downgrade*") -and ($command -like "*--ignore-dependencies*") } + } + + It "7.7.7 -> 8.9.8 should not contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "8.9.8" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -notlike "*--allow-downgrade*") -and ($command -notlike "*--ignore-dependencies*") } + } + + It "7.7.7 -> 7.6.7 should contain --allow-downgrade --ignore-dependencies" { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "7.6.7" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -ParameterFilter { ($command -like "*--allow-downgrade*") -and ($command -like "*--ignore-dependencies*") } + } + } + + Context "Skip Needless Installs" { + BeforeEach { + Mock Get-ChocoState -ModuleName $moduleForMock { return @{Name = "dummyPackage"; Version = "1.3.2" } } + } + It "Skips installing a package that already exists." { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.2" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -Times 0 + } + } + + Context "feat versions" { + BeforeEach { + Mock Get-ChocoState -ModuleName $moduleForMock { return @{Name = "dummyPackage"; Version = "1.3.2-feat-some-long-version-038" } } + } + It "1.3.2-feat-038 -> 1.3.2-feat-039 upgrade should trigger install." { + Invoke-ChocoInstallPackagesPrivate -packages @{Name = "dummyPackage"; Version = "1.3.2-feat-some-long-version-039" } + Assert-MockCalled Invoke-Expression -ModuleName $moduleForMock -Times 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.ps1 new file mode 100644 index 0000000..6619b9b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.ps1 @@ -0,0 +1,72 @@ +function Invoke-ProgetRequest { + <# +.SYNOPSIS + USed for making fault tolerant web requests to proget +.EXAMPLE + $response = Invoke-ProgetRequest -URI $uri -Headers $headers +.PARAMETER URI + Proget URI to make request against +.PARAMETER Headers + Generally used for auth header, but accepts any header +.OUTPUTS + Response from invoke web request to Proget +.NOTES + Uses Invoke-CommandWithRetry to retry requests to recover from transient failures +#> + [cmdletbinding()] + [OutputType([Microsoft.PowerShell.Commands.WebResponseObject])] + param ( + [Parameter(Mandatory = $true)] + $URI, + [Parameter(Mandatory = $true)] + $Headers + + ) + + if ($null -eq $Headers) { + $Headers = @{} + } + + $loglead = (Get-LogLeadName) + + $retryStatusCodes = @( + [System.Net.HttpStatusCode]::BadGateway, + [System.Net.HttpStatusCode]::BadRequest, + [System.Net.HttpStatusCode]::GatewayTimeout, + [System.Net.HttpStatusCode]::InternalServerError, + [System.Net.HttpStatusCode]::ServiceUnavailable + ) + + Write-Host "$logLead : Performing web request to $($URI)" + + $command = { + try { + $response = Invoke-WebRequest $URI -UseBasicParsing -Headers $headers + + if ($response.StatusCode -ne "200") { + throw "Non-success Statuscode found was $($response.StatusCode)" + } + } catch [System.Net.WebException] { + if ($null -eq $_.Exception.Response -and $_.Exception.Message -eq "The operation has timed out.") { + Write-Host "$logLead : No response object found. Request timed out. throwing" + throw $_ + } + } catch { + Write-Host "Exception $($_.Exception.Message)" + Write-Host "$logLead : Response code: $($response.StatusCode)" + if ($retryStatusCodes -contains $response.StatusCode) { + Write-Host "$logLead : retry code Detected, throwing" + throw $_ + } + if ($response.StatusCode -ne "200") { + Write-Host "Non-success Statuscode found was $($response.StatusCode)" + throw $_ + } + } + return $response + } + $response = Invoke-CommandWithRetry -ScriptBlock $command -Exponential + Write-Host "$logLead : Done Trying to $($URI)" + + return $response +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.tests.ps1 new file mode 100644 index 0000000..d52c936 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Invoke-ProgetRequest.tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$currentErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "stop" + +Describe "Invoke-ProgetRequest" { + Mock Write-Host + Mock Get-LogLeadName + Mock Write-Warning + Mock Invoke-WebRequest + + $URI = "fakeURI" + $Headers = @{boo = "fake Header" } + + Context "Assert mocks are called" { + Mock Invoke-CommandWithRetry { return @{StatusCode = "200" } } + It "Assert ICWR mock is called " { + mock Invoke-CommandWithRetry { return $null } + Invoke-ProgetRequest -URI $URI -Headers $Headers + Assert-MockCalled Invoke-CommandWithRetry + } + } + Context "Throw Checking" { + Mock Invoke-CommandWithRetry { throw [System.Net.WebException] "The operation has timed out." } + It "Throws when timing out" { + { Invoke-ProgetRequest -URI $URI -Headers $Headers -ErrorAction Stop} | Should -Throw + } + It "500 response code should throw" { + Mock Invoke-CommandWithRetry { return @{StatusCode = "500" } } + { Invoke-ProgetRequest -URI $URI -Headers $Headers -ErrorAction Stop} | Should -Not -Throw + } + It "200 response code should not throw" { + Mock Invoke-CommandWithRetry { return @{StatusCode = "200" } } + { Invoke-ProgetRequest -URI $URI -Headers $Headers -ErrorAction Stop} | Should -Not -Throw + } + It "300 response code should throw" { + Mock Invoke-CommandWithRetry { return @{StatusCode = "300" } } + { Invoke-ProgetRequest -URI $URI -Headers $Headers -ErrorAction Stop} | Should -Not -Throw + } + } +} + +$ErrorActionPreference = $currentErrorActionPreference \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Publish-ChocoPackage.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Publish-ChocoPackage.ps1 new file mode 100644 index 0000000..27182a9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Publish-ChocoPackage.ps1 @@ -0,0 +1,75 @@ +function Publish-ChocoPackage { +<# +.SYNOPSIS + Promotes a Chocolatey package from one feed to another. + WARNING: Even if a package of the correct version has already been promoted to the destination feed, a fresh Promotion will occur. + +.PARAMETER PackageName + Name of the package to Promote + +.PARAMETER Version + Version of package to Promote + +.PARAMETER ApiKey + API Key for Nuget Server + +.PARAMETER Comments + Comments to post to the promotion + +.PARAMETER FromFeed + Source feed FROM which to promote PackageName + +.PARAMETER ToFeed + Destination feed TO which to promote PackageName +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$PackageName, + [Parameter(Mandatory = $true)] + [string]$Version, + [Parameter(Mandatory = $true)] + [string]$ApiKey, + [Parameter(Mandatory = $true)] + [string]$Comments, + [Parameter(Mandatory = $false)] + [string]$FromFeed = "choco.Staging", + [Parameter(Mandatory = $false)] + [string]$ToFeed = "choco.Prod" + ) + $logLead = (Get-logLeadName) + + $baseUrl = "https://packagerepo.orb.alkamitech.com"; + + $promoteUrl = "$baseUrl/api/promotions/promote?key=$ApiKey" + + $body = @{ + "packageName" = $PackageName + "groupName" = "" + "version" = $Version + "fromFeed" = $FromFeed + "toFeed" = $ToFeed + "comments" = $Comments + } + + try { + $response = Invoke-WebRequest $promoteUrl -Body $body -Method "POST" + + $responseContent = [System.IO.StreamReader]::new($response.RawContentStream).ReadToEnd() + + #Proget returns an HTML page for blocked IPs. Hacky special case to handle it and potentially any others... + if ($response.StatusCode -eq 200 -and $responseContent.StartsWith("")) { + throw $responseContent + } + } catch { + # NOTE: Suppressing this exception means the catch in TDC's PromotePackages.ps1 has NOTHING + # to catch, and therefore, never outputs the intended warning + # This is ONLY called from the VerifyPackagesInFeed dependency from the PromotePackages.ps1 + # Allowing this to rethrow will allow that to catch and add a buildProblem + $thrownException = $_ + Write-Warning ("$logLead : ProGet Returned Error: $thrownException") + throw $thrownException + } + + Write-Host ("$logLead : Promotion complete. Proget response: $responseContent") +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Remove-OrphanedChocoFolders.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Remove-OrphanedChocoFolders.ps1 new file mode 100644 index 0000000..7ce5628 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Remove-OrphanedChocoFolders.ps1 @@ -0,0 +1,86 @@ +function Remove-OrphanedChocoFolders { + <# + .SYNOPSIS + Remove folders and files left behind by choco uninstall + .DESCRIPTION + Accepts either a package name or can search all packages to find choco packages with files left behind + .Notes + This funciton uses a parameter set for input parameters to determine if the function should crawl + all packages or inspect the folder of the package name provided + .EXAMPLE + This example Shows how to crawl all packages to look for orphaned folders + Remove-OrphanedChocoFolders -AllPackages + This example Shows how to specify a package name to inspect as an orphaned folder + Remove-OrphanedChocoFolders -PackageName "Alkami.ms.package.name" + #> + [cmdletbinding()] + param( + [Parameter(Mandatory = $true, ParameterSetName = "ByPackageName")] + [string]$PackageName, + [Parameter(Mandatory = $true, ParameterSetName = "ByAllPackages")] + [switch]$AllPackages + ) + + $logLead = Get-LogLeadName + + if (($PSCmdlet.ParameterSetName -eq "ByAllPackages") -and ($AllPackages -ne $true)) { + Write-Error "$logLead : You supplied -AllPackages, but it was not true, check your calling code" + return + } + Write-Host "$logLead : Searching for orphaned choco folders" + $chocoInstallPath = Get-ChocolateyInstallPath + $chocoLibDirectory = Join-Path $chocoInstallPath "lib" + If ([string]::IsNullOrEmpty($chocoLibDirectory) ) { + # While this is a problem, we dont want to halt the deploy pipline for this, rather we will report a build statistic problem + Write-Warning "$loglead : choco lib directory to search was null, stopping" + Write-Host "##teamcity[buildStatisticValue key='FailedToRemoveOrphanedFolder' value='1']" + return + } + if ($PSCmdlet.ParameterSetName -eq "ByPackageName" ) { + if ([string]::IsNullOrEmpty($PackageName) -eq $false) { + Write-Host "$logLead : Checking $PackageName" + $packageDirectory = Join-Path -Path $chocoLibDirectory -ChildPath $PackageName + $packageFolderContents = Get-ChildItem -Path $chocoLibDirectory -Filter $PackageName -ErrorAction SilentlyContinue + if ($null -ne $packageFolderContents) { + Remove-FileSystemItem -Path $packageDirectory -Force + Write-Host "##teamcity[buildStatisticValue key='SuccesfullyRemovedOrphanedFolder' value='1']" + return + } else { + Write-Host "$loglead : No folder found to remove for path $packageDirectory" + return + } + } else { + Write-Host "$logLead : PacakgeName was null or empty, but you supplied the PackageName param" + Write-Host "##teamcity[buildStatisticValue key='FailedToRemoveOrphanedFolder' value='1']" + return + } + } + if (($PSCmdlet.ParameterSetName -eq "ByAllPackages") -and ( $AllPackages -eq $true)) { + Write-Host "$logLead : Checking all folders" + $chocoLibFolders = Get-ChildItem -Path $chocoLibDirectory -Directory -Exclude "chocolatey" # exclude the chocolatey folder + + if ($null -eq $chocoLibFolders ) { + Write-Host "$logLead : No choco package folders found to search for missing nuspecs" + return + } + foreach ($chocoLibFolder in $chocoLibFolders) { + Write-Host "$loglead : Interogating $chocoLibFolder" + $nuspecPath = $null + $nuspecPath = Get-ChildItem -Path $chocoLibFolder.FullName -Filter "*.nuspec" + if ($null -eq $nuspecPath) { + Write-Warning "$logLead : Could not find nuspec for $chocoLibFolder" + try { + Remove-FileSystemItem -Path $chocoLibFolder -Force + Write-Host "##teamcity[buildStatisticValue key='SuccesfullyRemovedOrphanedFolder' value='1']" + } catch { + Write-Warning "$logLead : Unable to remove folder $chocoLibFolder" + Write-Warning $_ + Write-Host "##teamcity[buildStatisticValue key='FailedToRemoveOrphanedFolder' value='1']" + } + } else { + Write-Host "$logLead : Found nuspec for $chocoLibFolder" + continue + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.Tests.ps1 new file mode 100644 index 0000000..68ac484 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.Tests.ps1 @@ -0,0 +1,377 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Remove-PackagesThatAreAlreadyInstalled" { + + function Get-PackageObject{ + param( + $packageName, + $packageVersion, + [switch]$forceSameVersion + ) + + if($forceSameVersion){ + $properties = @{ Name = $packageName; Version = $packageVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null; ForceSameVersion = $true} + } else { + $properties = @{ Name = $packageName; Version = $packageVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null; ForceSameVersion = $false} + } + $package = New-Object -TypeName PSObject -Prop $properties; + + return $package + } + + Context "When Provided A Null Or Empty Package List" { + + $AppPackagesToInstall = @() + + It "Returns Null"{ + Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $AppPackagesToInstall | Should -Be $null + Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $null | Should -Be $null + } + } + + Context "When Provided Packages That Aren't Installed" { + $packages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "alkami.fake.package1" -packageVersion "1.1.1" + $packageObject2 = Get-PackageObject -packageName "alkami.fake.package2" -packageVersion "1.1.1" + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1, $pkg2) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{} + + PackageToServers = @{} + } + + It "Returns Those Packages"{ + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 2 + $results[0].Name | Should -Be "alkami.fake.package1" + $results[0].Version | Should -Be "1.1.1" + $results[1].Name | Should -Be "alkami.fake.package2" + $results[1].Version | Should -Be "1.1.1" + } + } + + Context "When Provided Packages To Install That Are Also On The Uninstall List"{ + + $packages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "alkami.fake.package1" -packageVersion "1.1.1" + $packageObject2 = Get-PackageObject -packageName "alkami.fake.package2" -packageVersion "1.1.1" + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @() + WebPackagesToUninstall = @() + AppPackagesToUninstall = @($pkg1, $pkg2) + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{} + + PackageToServers = @{} + } + + It "Returns Those Packages"{ + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 2 + $results[0].Name | Should -Be "alkami.fake.package1" + $results[0].Version | Should -Be "1.1.1" + $results[1].Name | Should -Be "alkami.fake.package2" + $results[1].Version | Should -Be "1.1.1" + } + } + + Context "When Provided Force Reinstall Packages" { + $packages = [System.Collections.ArrayList]::new() + + $packageName1 = "alkami.fake.package1" + $packageVersion1 = "1.1.1" + $packageName2 = "alkami.fake.package2" + $packageVersion2 = "1.1.1" + + $packageObject1 = Get-PackageObject -packageName $packageName1 -packageVersion $packageVersion1 -forceSameVersion + $packageObject2 = Get-PackageObject -packageName $packageName2 -packageVersion $packageVersion2 -forceSameVersion + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + + Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock + + It "Returns Those Packages If We Are Force Reinstalling"{ + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1, $pkg2) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = $true + PackageToVersions = @{$packageName1 = $packageVersion1; $packageName2 = $packageVersion2} + + PackageToServers = @{} + } + + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 2 + $results[0].Name | Should -Be "alkami.fake.package1" + $results[0].Version | Should -Be "1.1.1" + $results[1].Name | Should -Be "alkami.fake.package2" + $results[1].Version | Should -Be "1.1.1" + } + + It "Does Not Return Those Packages If We Are Not Force Installing"{ + + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1, $pkg2) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = $false + PackageToVersions = @{$packageName1 = $packageVersion1; $packageName2 = $packageVersion2} + + PackageToServers = @{} + } + + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 0 + } + } + + Context "When Provided A Package Which Has Multiple Versions Installed"{ + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.fake.package1" + $packageVersion1 = "1.1.1" + $packageVersion2 = "1.1.2" + $packageVersion3 = "1.1.3" + + $packageObject1 = Get-PackageObject -packageName $packageName1 -packageVersion $packageVersion1 -forceSameVersion + + $packages.Add($packageObject1) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{} + + PackageToServers = @{} + } + + $metadata.PackageToVersions[$packageName1] = @($packageVersion1) + $metadata.PackageToVersions[$packageName1] = $metadata.PackageToVersions[$packageName1] + $packageVersion2 + $metadata.PackageToVersions[$packageName1] = $metadata.PackageToVersions[$packageName1] + $packageVersion3 + + It "Returns That Package"{ + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 1 + $results[0].Name | Should -Be "alkami.fake.package1" + # This should match whatever was in $packages. The currently installed packages don't matter. + $results[0].Version | Should -Be "1.1.1" + } + } + + Context "When Provided A New Version Of An Already Installed Package"{ + $packages = [System.Collections.ArrayList]::new() + + $packageName1 = "alkami.fake.package1" + $packageVersion1 = "1.1.1" + $packageVersion2 = "1.1.3" + + $packageObject1 = Get-PackageObject -packageName $packageName1 -packageVersion $packageVersion2 -forceSameVersion + $packages.Add($packageObject1) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{} + + PackageToServers = @{} + } + + $metadata.PackageToVersions[$packageName1] = @($packageVersion1) + + It "Returns That Package"{ + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 1 + $results[0].Name | Should -Be "alkami.fake.package1" + # This should match whatever was in $packages. + $results[0].Version | Should -Be "1.1.3" + } + } + + Context "When Provided A Package Already Installed On Some Servers But Not All"{ + $packages = [System.Collections.ArrayList]::new() + $packageName1 = "alkami.fake.package1" + $packageVersion1 = "1.1.1" + + # leaving this one alone because it's the only place that needs this one property. Easier than modifying the function further. + $properties1 = @{ Name = $packageName1; Version = $packageVersion1; Feed = $null; Tags = $null; IsService = $null; StartMode = $null; InstallToMic = $true} + $pkg1 = New-Object -TypeName PSObject -Prop $properties1; + $packages.Add($pkg1) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyAppTestServer") + MicServers = @("MyMicTestServer") + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{} + + PackageToServers = @{} + } + + $metadata.PackageToVersions[$packageName1] = @($packageVersion1) + + It "Returns That Package"{ + [array]$results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 1 + $results[0].Name | Should -Be "alkami.fake.package1" + $results[0].Version | Should -Be "1.1.1" + } + } + + Context "When Provided A Package And Version Already On All Specified Servers" { + $packages = [System.Collections.ArrayList]::new() + + $packageName1 = "alkami.fake.package" + $packageVersion1 = "1.1.1" + + $packageObject1 = Get-PackageObject -packageName $packageName1 -packageVersion $packageVersion1 -forceSameVersion + $packages.Add($packageObject1) + + $metadata = New-Object psobject -property @{ + WebPackagesToInstall = @() + AppPackagesToInstall = @($pkg1) + WebPackagesToUninstall = @() + AppPackagesToUninstall = @() + + HasWebUninstalls = $false + HasAppUninstalls = $false + HasMicUninstalls = $false + + WebServers = @() + AppServers = @("MyTestServer") + MicServers = @() + FabServers = @() + + ForceReinstallPackages = "" + PackageToVersions = @{ "alkami.fake.package" = "1.1.1"} + + PackageToServers = @{"alkami.fake.package" = @("MyTestServer")} + } + + Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock + + + It "Writes To Console That The Package Is Being Removed" { + + Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + Assert-MockCalled -CommandName Write-Host -ModuleName $moduleForMock -ParameterFilter { + $Object -match "Package alkami.fake.package is already installed on all servers. Removing from list of packages to deploy" + } -Times 1 -Exactly + } + + It "Removes The Package" { + $results = Remove-PackagesThatAreAlreadyInstalled -PackagesToInstall $packages -PackageMetadata $metadata + + $results.Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.ps1 new file mode 100644 index 0000000..0ecf790 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Remove-PackagesThatAreAlreadyInstalled.ps1 @@ -0,0 +1,118 @@ +function Remove-PackagesThatAreAlreadyInstalled { + <# +.SYNOPSIS + Given a list of packages, and a hashtable of packagename->unique-versions-of-package, return a list of packages + that actually need to be installed. +.PARAMETER PackagesToInstall + An array of packages to be installed +.PARAMETER PackageMetadata + The giant list of all the things from Classify Packages. + .PARAMETER DebugMetadata + Not used, but supplied from Classify Packages. Intentionally left here with the expectation of adding additional properties to it which might be passed through. +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("DebugMetadata", '', Justification="Intentionally left here with the expectation of adding additional properties to it which might be passed through.", Scope = "Function")] + param ( + [array]$PackagesToInstall, + $PackageMetadata, + $DebugMetadata + ) + + if (Test-IsCollectionNullOrEmpty $PackagesToInstall) { + return @() + } + + # Declare a hashtable to track which packages we are going to preserve from the install list. + $packagesToKeep = @{ } + + # Cache the combinations of serversToInstall so we don't recreate them / resize arrays thousands of times. + $serversToInstallCache = @{} + + # Filter packages to be installed by what is installed in the environment. + foreach ($package in $PackagesToInstall) { + $lowerName = $package.Name.ToLower() + $packageIsInstalled = $PackageMetadata.PackageToVersions.ContainsKey($lowerName) + + $packageIsInUninstallList = ($PackageMetadata.WebPackagesToUninstall.Name -contains $package.Name) -or ($PackageMetadata.AppPackagesToUninstall.Name -contains $package.Name) + + # Install the package if it's not installed at all. + if (!$packageIsInstalled) { + $packagesToKeep[$lowerName] = $true + continue + } + + # Install the package if it is in either uninstall list + # Case 1 - Uninstalling from App to Install on Web in the case of a misplaced widget + # Case 2 - Forcing an uninstall/reinstall - maybe the wrong version got installed or partly installed + if ($packageIsInUninstallList) { + $packagesToKeep[$lowerName] = $true + continue + } + + # Otherwise figure out if we need to install/force-reinstall the package. + # Get the different versions of the package on-hand. + [array]$versionsOfPackage = $PackageMetadata.PackageToVersions[$lowerName] + + # We are reinstalling ORB + if ($PackageMetadata.ForceReinstallPackages) { + # If it's a package type that is reinstalled with ORB, or must reinstall for the same version of the package, keep the package. + if ($package.ReinstallWithORB -or $package.ForceSameVersion) { + $packagesToKeep[$lowerName] = $true + continue + } + } + + # If there are multiple versions of the package installed, keep the package. + # Hopefully this will standardize that same version everywhere. + if ($versionsOfPackage.Count -gt 1) { + $packagesToKeep[$lowerName] = $true + continue + } + + # Otherwise if there is one version of the package, keep it only if it is NOT the version we are trying to install. + if ($packageIsInstalled -and ($versionsOfPackage.Count -eq 1) -and ($versionsOfPackage[0] -ne $package.Version)) { + $packagesToKeep[$lowerName] = $true + continue + } + + # Make sure that this package is on all of the servers its supposed to be installed on. + # Produce the combination of servers that the package needs to be installed to. + # Cache the combinations of serversToInstall so that we don't recreate them thousands of times. + $serversToInstallKey = 1 * [int]($package.InstallToWeb) + + 2 * [int]($package.InstallToFab) + + 4 * [int]($package.InstallToMic) + + 8 * [int]($package.InstallToApp) + if($serversToInstallCache.ContainsKey($serversToInstallKey)) { + $serversToInstall = $serversToInstallCache[$serversToInstallKey] + } else { + $serversToInstall = @() + if($package.InstallToWeb) { $serversToInstall += $PackageMetadata.WebServers; } + if($package.InstallToFab) { $serversToInstall += $PackageMetadata.FabServers; } + if($package.InstallToMic) { $serversToInstall += $PackageMetadata.MicServers; } + if($package.InstallToApp) { $serversToInstall += $PackageMetadata.AppServers; } + $serversToInstall = $serversToInstall | Where-Object { $null -ne $_ } + $serversToInstallCache[$serversToInstallKey] = $serversToInstall + } + + # If the package is not installed on all of the servers it should be installed on, then keep the package. + $installedServers = $PackageMetadata.PackageToServers[$lowerName] + $installedToAllServers = $true + foreach($server in $serversToInstall) { + if($installedServers -notcontains $server) { + $installedToAllServers = $false + break + } + } + if(!$installedToAllServers -or $packageIsInUninstallList) { + $packagesToKeep[$lowerName] = $true + continue + } + + Write-Host "Package $($package.Name) is already installed on all servers. Removing from list of packages to deploy" + # Now only packages we care to install/reinstall are in $packagesToKeep! + } + + # Filter the install packages down to the packages we intend on keeping. + [array]$PackagesToInstall = $PackagesToInstall | Where-Object { $packagesToKeep.ContainsKey($_.Name.ToLower()) } + return $PackagesToInstall +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.Tests.ps1 new file mode 100644 index 0000000..84a06b6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.Tests.ps1 @@ -0,0 +1,101 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-InstallPackages" { + function Get-PackageObject{ + param( + $packageName, + $packageVersion + ) + + $properties = @{ Name = $packageName; Version = $packageVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $package = New-Object -TypeName PSObject -Prop $properties; + + return $package + } + + Context "When InstallPackages is Null or Empty"{ + + $packages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService2" -packageVersion "1.0.0" + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + + It "Returns Packages on Null"{ + $installPackages = $null + $returnedInstallPackages = Select-InstallPackages -Packages $packages -InstallPackages $installPackages + + $returnedInstallPackages | Should -Be $packages + } + + It "Returns Packages on Empty"{ + $installPackages = [System.Collections.ArrayList]::new() + $returnedInstallPackages = Select-InstallPackages -Packages $packages -InstallPackages $installPackages + + $returnedInstallPackages | Should -Be $packages + } + } + + Context "When Given a Packages Array with Null Entries"{ + $packages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService2" -packageVersion "1.0.0" + $packageObject3 = $null + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + $packages.Add($packageObject3) + + It "Strips Null Entries"{ + $installPackages = [System.Collections.ArrayList]::new() + $returnedInstallPackages = Select-InstallPackages -Packages $packages -InstallPackages $installPackages + + $returnedInstallPackages.Count | Should -Be 2 + } + } + + Context "When Given the Same Package in Both Arrays" { + + $packages = [System.Collections.ArrayList]::new() + $installPackages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.7.2" + + $packages.Add($packageObject1) + $installPackages.Add($packageObject2) + + It "Takes the Value from InstallPackages" { + $returnedInstallPackages = Select-InstallPackages -Packages $packages -InstallPackages $installPackages + + $returnedInstallPackages[0].Version | Should -Be "1.7.2" + } + } + Context "When Given New Packages In InstallPackages"{ + + $packages = [System.Collections.ArrayList]::new() + $installPackages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService2" -packageVersion "1.0.0" + + $packages.Add($packageObject1) + $installPackages.Add($packageObject2) + + It "Adds the New Values to Packages"{ + $returnedInstallPackages = Select-InstallPackages -Packages $packages -InstallPackages $installPackages + + $returnedInstallPackages.Name -Contains 'Alkami.FakeService1' | Should -BeTrue + $returnedInstallPackages.Name -Contains 'Alkami.FakeService2' | Should -BeTrue + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.ps1 new file mode 100644 index 0000000..2d861ad --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Select-InstallPackages.ps1 @@ -0,0 +1,54 @@ +function Select-InstallPackages { +<# +.SYNOPSIS + Overwrites packages that share the same from the first list, with the second list. + Makes sure everything in the second list is in the result. + Used for overwriting the packages from the environment with packages intended to be installed. + Used during a full deploy + +.PARAMETER Packages + The list of packages currently installed in an environment +.PARAMETER InstallPackages + The list of packages to be installed in an environment. +#> + + [CmdletBinding()] + param ( + [array]$Packages, + [array]$InstallPackages + ) + + # Strip null entries out of the packages. + $Packages = $Packages | Where-Object { $null -ne $_ } + $InstallPackages = $InstallPackages | Where-Object { $null -ne $_ } + + # Just return the original list if the install packages are null/empty. + if (Test-IsCollectionNullOrEmpty $InstallPackages) { + return $Packages + } + + # Load the install packages into a map for quick lookup. + $installMap = @{ } + foreach ($package in $InstallPackages) { + $installMap[$package.Name.ToLower()] = $package + } + + # If the package name exists in $installPackages, overwrite the package from $packages. + $result = @() + foreach ($package in $Packages) { + $name = $package.Name.ToLower() + if ($installMap.ContainsKey($name)) { + $result += $installMap[$name] + $installMap.Remove($name) + } else { + $result += $package + } + } + + # Anything left in $installMap is a new package. Add them in. + if ($installMap.Count -gt 0) { + $result += $installMap.Values + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.Tests.ps1 new file mode 100644 index 0000000..037882a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.Tests.ps1 @@ -0,0 +1,83 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-UniqueServerPackages" { + + function Get-PackageObject{ + param( + $packageName, + $packageVersion + ) + + $properties = @{ Name = $packageName; Version = $packageVersion; Feed = $null; Tags = $null; IsService = $null; StartMode = $null} + $package = New-Object -TypeName PSObject -Prop $properties; + + return $package + } + + Context "When Supplied Package Array Is Empty" { + $packages = [System.Collections.ArrayList]::new() + + $uniquePackages = Select-UniqueServerPackages $packages + + It "Returns Null"{ + $uniquePackages | Should -Be $null + } + } + + Context "When Supplied Package Array Has a Single Entry" { + $packages = [System.Collections.ArrayList]::new() + + $packageObject = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + + $packages.Add($packageObject) + + $uniquePackages = [array](Select-UniqueServerPackages $packages) + + It "Returns a Single Entry"{ + $uniquePackages.Count | Should -Be 1 + } + } + + Context "When Supplied Duplicate Packages" { + $packages = [System.Collections.ArrayList]::new() + + # Define 3 duplicate packages. + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject3 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + $packages.Add($packageObject3) + + $uniquePackages = [array](Select-UniqueServerPackages $packages) + + It "Returns Unique Entries"{ + $uniquePackages.Count | Should -Be 1 + } + } + + Context "When Supplied Unique Pakcages"{ + $packages = [System.Collections.ArrayList]::new() + + $packageObject1 = Get-PackageObject -packageName "Alkami.FakeService1" -packageVersion "1.0.0" + $packageObject2 = Get-PackageObject -packageName "Alkami.FakeService2" -packageVersion "1.0.0" + $packageObject3 = Get-PackageObject -packageName "Alkami.FakeService3" -packageVersion "1.0.0" + + $packages.Add($packageObject1) + $packages.Add($packageObject2) + $packages.Add($packageObject3) + + $uniquePackages = [array](Select-UniqueServerPackages $packages) + + It "Returns All Entries"{ + $uniquePackages.Count | Should -Be 3 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.ps1 new file mode 100644 index 0000000..83cab54 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Select-UniqueServerPackages.ps1 @@ -0,0 +1,48 @@ +function Select-UniqueServerPackages { +<# +.SYNOPSIS + Given a list of package lists (ie per server), this function returns the unique packages from each list of packages. + If there is a higher version of a package in one package list than another, the higher version is preferred. + +.PARAMETER PackagesArray + Array of packages to process +#> + [CmdletBinding()] + param ( + [array]$PackagesArray + ) + + if (Test-IsCollectionNullOrEmpty $PackagesArray) { + Write-Verbose "Array was null." + return $null + } + + if ($PackagesArray.Count -eq 1) { + Write-Verbose "Only one value in array. Returning it." + return $PackagesArray[0] + } + + # Determine the combined unique list of packages. + Write-Verbose "Looping through packages..." + $uniquePackages = @{ } + foreach ($packages in $PackagesArray) { + if ($null -eq $packages) { + continue + } + foreach ($package in $packages) { + $name = $package.Name.ToLower() + if (!($uniquePackages.ContainsKey($name))) { + # If this package is not in the unique package list, add it! + Write-Verbose "Found a package: $package" + $uniquePackages[$name] = $package + } elseif ($package.Version -gt $uniquePackages[$name].Version) { + # Otherwise if the version of the package on this server is greater than what is stored, prefer the higher version. + Write-Verbose "Found a package again! $package" + $uniquePackages[$name] = $package + } + } + } + + Write-Verbose "Returning unique values." + return $uniquePackages.Values +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeeds.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeeds.ps1 new file mode 100644 index 0000000..6add99d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeeds.ps1 @@ -0,0 +1,178 @@ +function Set-ChocoPackageSourceFeeds { + <# +.SYNOPSIS + Assigns package sources to the $package.Feed of the packages passed in. + THIS IS A SIDE EFFECT because collections in PowerShell are byRef, not byValue + +.PARAMETER Packages + Array of Package Objects to assign Feed members to + +.PARAMETER Hostname + Hostname of computer ON WHICH to get sources and package list for those sources. Default is localhost. + +.PARAMETER MissingPackageLogLevel + Whether to Write-Error or Write-Warning when there are missing packages. Default is ERROR. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [object[]]$Packages, + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost", + [Parameter(Mandatory = $false)] + [ValidateSet("ERROR", "WARNING")] + [string]$MissingPackageLogLevel = "ERROR" + ) + $logLead = (Get-logLeadName) + + # Early out if all of the packages already have feeds. + if ($Packages.Feed -notcontains $null) { + Write-Warning "$logLead : All packages have feeds already, no work to do, returning early." + return + } + + $PASSTHRU_CHOCOLATEY_PUBLIC_URL = Get-ChocoPublicPassThruFeedUrl + $REAL_CHOCOLATEY_PUBLIC_URL = "https://chocolatey.org/api/v2*" + Write-Host "$logLead : Now determining source feeds for packages." + + # Grab feeds from the server, and filter out the chocolatey/nameless feeds. + $feeds = [array](Get-ChocolateySources -hostname $Hostname -includeDisabledSources) + + # Get the public choco feed passthru. + $passthroughPublicFeed = $feeds | Where-Object { $_.Source -eq $PASSTHRU_CHOCOLATEY_PUBLIC_URL } + + # Get the other feeds where Name, Source are not empty, are not Disabled and are not the Passthru feed. + $filteredFeeds = $feeds | Where-Object { + (![string]::IsNullOrWhiteSpace($_.Name) -and + ![string]::IsNullOrWhiteSpace($_.Source)) -and + !$_.Disabled -and + $_.Source -ne $PASSTHRU_CHOCOLATEY_PUBLIC_URL -and + $_.Source -notlike $REAL_CHOCOLATEY_PUBLIC_URL + } + + # Get feeds where (Name OR Source) is empty AND NOT Disabled. These are invalid feeds. + $invalidFeeds = $feeds | Where-Object { + ([string]::IsNullOrWhiteSpace($_.Name) -or + [string]::IsNullOrWhiteSpace($_.Source)) -and + !$_.Disabled + } + + # Get any feeds that are Disabled. + $disabledFeeds = $feeds | Where-Object { $_.Disabled } + + if (!(Test-IsCollectionNullOrEmpty $invalidFeeds)) { + Write-Warning "$logLead : the following feeds are misconfigured`n$($invalidFeeds.Name)" + } + + if(Test-IsCollectionNullOrEmpty $filteredFeeds) { + Write-Error "$logLead : No filtered feeds found!" + throw "$logLead : No filtered feeds found!" + return + } + + # These feeds were flagged as disabled so we'll let everyone know we're skipping them. + foreach ($disabledFeed in $disabledFeeds) { + Write-Host "$logLead : Feed '$($disabledFeed.Name)' is flagged as Disabled, ignoring." + } + + # If there is not public choco feed configured, add one that is the Proget passthru. + if ($null -eq $passthroughPublicFeed) { + $properties = @{ + Name = 'Chocolatey Passthrough' + Source = $PASSTHRU_CHOCOLATEY_PUBLIC_URL + Priority = 0 + Disabled = $false + IsDefault = $true + IsSDK = $false + }; + $passthroughPublicFeed = New-Object -TypeName PSObject -Prop $properties; + Write-Host "$logLead : Setting default choco passthru '$($passthroughPublicFeed.Name)' '$($passthroughPublicFeed.Source)'" + } + + # Get all of the packages from each feed in parallel. + Write-Host "$logLead : Get all packages from each feed in parallel." + $feedPackageResults = Invoke-Parallel -objects $filteredFeeds -returnObjects -arguments $Hostname -script { + Param( + $sbFeed, + $sbHostname + ) + + # Collect the names of packages in this feed. + $feedName = $sbFeed.Name + $feedSource = $sbFeed.Source + $loglead = "[Set-ChocoPackageSourceFeeds]" + + Write-Host "$loglead : Pulling packages names from source '$feedName' - '$feedSource'" + + $scriptPackages = @() + $scriptPackages = Get-ChocoState -s $feedSource -pre + + if (($scriptPackages.Count -eq 0) -or (($scriptPackages.Count -eq 1) -and ([string]::IsNullOrWhiteSpace($scriptPackages[0])))) { + $scriptPackages = $null + } + + $result = @{ + FeedName = $sbFeed.Name + Packages = $scriptPackages + } + return $result + } + + if(!(Test-IsCollectionNullOrEmpty $feedPackageResults)) { + $feedPackageResults = $feedPackageResults | Where-Object { $null -ne ($_["Packages"]) } + } + + # Match every package name to a feed. + $packageToFeed = @{} + + foreach ($feedResult in $feedPackageResults) { + $feedName = $feedResult["FeedName"] + $feed = $filteredFeeds | Where-Object { $_.Name -eq $feedName } + [array]$packagesInFeed = $feedResult["Packages"] + + foreach ($feedPackage in $packagesInFeed) { + $packageName = $feedPackage.Name + if ($packageToFeed.ContainsKey($packageName)) { + $firstSourceName = $packageToFeed[$packageName].Name + Write-Error "$logLead : Multiple sources contain a package named '$packageName'. Found in sources '$firstSourceName' and '$feedName'" + } + $packageToFeed.Set_Item($packageName, $feed) + } + } + + # Run back through all the packages, and assign feeds based on package names. + foreach ($pkg in $Packages) { + $packageName = $pkg.Name + if ($packageToFeed.ContainsKey($packageName)) { + $pkg.Feed = $packageToFeed.Get_Item($packageName) + } else { + # For "Disabled" choco sources, you can only query them using the source param with a feed URL + # For public packages, "exact" matching is recommended. OpenSSH returns 8 records, only 1 is actually "openssh" + Write-Host "$logLead : Checking chocolatey.org passthru for '$packageName'" + $packagePublicRecord = Get-ChocoState -source $PASSTHRU_CHOCOLATEY_PUBLIC_URL -packageName $packageName -pre -exact + if ($null -eq $packagePublicRecord) { + $pkg.Feed = $null + } else { + $pkg.Feed = $passthroughPublicFeed + Write-Warning "$logLead : Package ONLY found on passthru for public chocolatey feed '$packageName'" + } + } + + if (!$pkg.Feed) { + Write-Warning "$logLead : Was not able to determine a source feed for package '$packageName'" + } + } + [string]$missingPackageOutputText = $null + $missingFeedPackages = $Packages | Where-Object { $null -eq $_.Feed } + foreach ($missingFeedPackage in $missingFeedPackages) { + $missingPackageOutputText += "$($missingFeedPackage.Name)|$($missingFeedPackage.Version)`n" + } + if (![string]::IsNullOrWhiteSpace($missingPackageOutputText)) { + if ($MissingPackageLogLevel -eq "ERROR") { + Write-Error "$logLead : Was not able to find the following package(s) in any feed:`n $missingPackageOutputText" + } else { + Write-Warning "$logLead : Was not able to find the following package(s) in any feed:`n $missingPackageOutputText" + } + } + Write-Host "$logLead : Package names matched to feeds." +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeedsV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeedsV2.ps1 new file mode 100644 index 0000000..8e37d68 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageSourceFeedsV2.ps1 @@ -0,0 +1,184 @@ +function Set-ChocoPackageSourceFeedsV2 { +<# +.SYNOPSIS + Assigns package sources to the $package.Feed of the packages passed in. + THIS IS A SIDE EFFECT because collections in PowerShell are byRef, not byValue + +.PARAMETER Packages + Array of Package Objects to assign Feed members to + +.PARAMETER Hostname + Hostname of computer ON WHICH to get sources and package list for those sources. Default is localhost. + +.PARAMETER MissingPackageLogLevel + Whether to Write-Error or Write-Warning when there are missing packages. Default is ERROR. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [object[]]$Packages, + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost", + [Parameter(Mandatory = $false)] + [ValidateSet("ERROR", "WARNING")] + [string]$MissingPackageLogLevel = "ERROR", + [switch]$excludeDisabledSources + ) + $logLead = (Get-logLeadName) + + # Early out if all of the packages already have feeds. + if ($Packages.Feed -notcontains $null) { + Write-Warning "$logLead : All packages have feeds already, no work to do, returning early." + + return + } + + $PASSTHRU_CHOCOLATEY_PUBLIC_URL = Get-ChocoPublicPassThruFeedUrl + $REAL_CHOCOLATEY_PUBLIC_URL = "https://chocolatey.org/api/v2*" + Write-Host "$logLead : Now determining source feeds for packages." + + # Grab feeds from the server, and filter out the chocolatey/nameless feeds. + $getChocoSourceArgs = @{ Hostname = $Hostname; } + if (!$excludeDisabledSources) { + $getChocoSourceArgs.includeDisabledSources = $true + } + + # Grab feeds from the server, and filter out the chocolatey/nameless feeds. + $feeds = [array](Get-ChocolateySourcesV2 @getChocoSourceArgs) + + # Get the public choco feed passthru. + $passthroughPublicFeed = $feeds | Where-Object { $_.Source -eq $PASSTHRU_CHOCOLATEY_PUBLIC_URL } + + # Get the other feeds where Name, Source are not empty, are not Disabled and are not the Passthru feed. + $filteredFeeds = $feeds | Where-Object { + (![string]::IsNullOrWhiteSpace($_.Name) -and + ![string]::IsNullOrWhiteSpace($_.Source)) -and + !$_.Disabled -and + $_.Source -ne $PASSTHRU_CHOCOLATEY_PUBLIC_URL -and + $_.Source -notlike $REAL_CHOCOLATEY_PUBLIC_URL + } + + # Get feeds where (Name OR Source) is empty AND NOT Disabled. These are invalid feeds. + $invalidFeeds = $feeds | Where-Object { + ([string]::IsNullOrWhiteSpace($_.Name) -or + [string]::IsNullOrWhiteSpace($_.Source)) -and + !$_.Disabled } + + # Get any feeds that are Disabled. + $disabledFeeds = $feeds | Where-Object { $_.Disabled } + + if (!(Test-IsCollectionNullOrEmpty $invalidFeeds)) { + Write-Warning "$logLead : the following feeds are misconfigured`n$($invalidFeeds.Name)" + } + + if(Test-IsCollectionNullOrEmpty $filteredFeeds) { + Write-Error "$logLead : No filtered feeds found!" + throw "$logLead : No filtered feeds found!" + return + } + + # These feeds were flagged as disabled so we'll let everyone know we're skipping them. + foreach ($disabledFeed in $disabledFeeds) { + Write-Host "$logLead : Feed '$($disabledFeed.Name)' is flagged as Disabled, ignoring." + } + + # If there is not public choco feed configured, add one that is the Proget passthru. + if ($null -eq $passthroughPublicFeed) { + $properties = @{ + Name = 'Chocolatey Passthrough' + Source = $PASSTHRU_CHOCOLATEY_PUBLIC_URL + Priority = 0 + Disabled = $false + IsDefault = $true + IsSDK = $false + }; + $passthroughPublicFeed = New-Object -TypeName PSObject -Prop $properties; + Write-Host "$logLead : Setting default choco passthru '$($passthroughPublicFeed.Name)' '$($passthroughPublicFeed.Source)'" + } + + # Get all of the packages from each feed in parallel. + $feedPackageResults = Invoke-Parallel -objects $filteredFeeds -returnObjects -arguments $Hostname -script { + Param( + $sbFeed, + $sbHostname + ) + + # Collect the names of packages in this feed. + $feedName = $sbFeed.Name + $feedSource = $sbFeed.Source + $logLead = "[Set-ChocoPackageSourceFeedsV2]" + + Write-Host "$logLead : Pulling packages names from source '$feedName' - '$feedSource'" + + $scriptPackages = @() + $scriptPackages = Get-ChocoState -s $feedSource -pre + + if(($scriptPackages.Count -eq 0) -or (($scriptPackages.Count -eq 1) -and ([string]::IsNullOrWhiteSpace($scriptPackages[0])))) { + $scriptPackages = $null + } + + $result = @{ + FeedName = $sbFeed.Name + Packages = $scriptPackages + } + return $result + } + + if(!(Test-IsCollectionNullOrEmpty $feedPackageResults)) { + $feedPackageResults = $feedPackageResults | Where-Object { $null -ne ($_["Packages"]) } + } + + # Match every package name to a feed. + $packageToFeed = @{} + + foreach($feedResult in $feedPackageResults) { + $feedName = $feedResult["FeedName"] + $feed = $filteredFeeds | Where-Object { $_.Name -eq $feedName } + [array]$packagesInFeed = $feedResult["Packages"] + + foreach ($feedPackage in $packagesInFeed) { + $packageName = $feedPackage.Name + if ($packageToFeed.ContainsKey($packageName)) { + $firstSourceName = $packageToFeed[$packageName].Name + Write-Error "$logLead : Multiple sources contain a package named '$packageName'. Found in sources '$firstSourceName' and '$feedName'" + } + $packageToFeed.Set_Item($packageName, $feed) + } + } + + # Run back through all the packages, and assign feeds based on package names. + foreach ($pkg in $Packages) { + $packageName = $pkg.Name + if ($packageToFeed.ContainsKey($packageName)) { + $pkg.Feed = $packageToFeed.Get_Item($packageName) + } else { + # For "Disabled" choco sources, you can only query them using the source param with a feed URL + # For public packages, "exact" matching is recommended. OpenSSH returns 8 records, only 1 is actually "openssh" + Write-Host "$logLead : Checking chocolatey.org passthru for '$packageName'" + $packagePublicRecord = Get-ChocoState -source $PASSTHRU_CHOCOLATEY_PUBLIC_URL -packageName $packageName -pre -exact + if ($null -eq $packagePublicRecord) { + $pkg.Feed = $null + } else { + $pkg.Feed = $passthroughPublicFeed + Write-Warning "$logLead : Package ONLY found on passthru for public chocolatey feed '$packageName'" + } + } + + if (!$pkg.Feed) { + Write-Warning "$logLead : Was not able to determine a source feed for package '$packageName'" + } + } + [string]$missingPackageOutputText = $null + $missingFeedPackages = $Packages | Where-Object { $null -eq $_.Feed } + foreach ($missingFeedPackage in $missingFeedPackages) { + $missingPackageOutputText += "$($missingFeedPackage.Name)|$($missingFeedPackage.Version)`n" + } + if (![string]::IsNullOrWhiteSpace($missingPackageOutputText)) { + if ($MissingPackageLogLevel -eq "ERROR") { + Write-Error "$logLead : Was not able to find the following package(s) in any feed:`n $missingPackageOutputText" + } else { + Write-Warning "$logLead : Was not able to find the following package(s) in any feed:`n $missingPackageOutputText" + } + } + Write-Host "$logLead : Package names matched to feeds." +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageTags.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageTags.ps1 new file mode 100644 index 0000000..4c588c2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Set-ChocoPackageTags.ps1 @@ -0,0 +1,69 @@ +function Set-ChocoPackageTags { +<# +.SYNOPSIS + Assigns package tags to the $package.Tags property of the package objects passed in. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [object[]]$packages + ) + $logLead = (Get-logLeadName) + + # Make sure that every package has its source feed assigned. + Set-ChocoPackageSourceFeeds($packages); + + Write-Host "$logLead : Now searching choco for package tags."; + + # Get the verbose choco-search of every package and parse tags. + foreach ($package in $packages) { + # If package tags are already assigned, move along. + if ($package.Tags) { + continue; + } + + # Parse tags from the verbose output of a choco search. + $name = $package.Name; + Write-Host "$logLead : Pulling tags for package '$name'"; + + if ($package.Feed) { + $verboseOutput = (choco search $name -verbose -e -s $package.Feed.Source ); + } + else { + $verboseOutput = (choco search $name -verbose -e ); + } + + # Verbose output of an existing package is longer than a single line! + if ($verboseOutput.count -le 1) { + Write-Host "$logLead : No tags found for package '$name'"; + continue; + } + + $tagLine = $verboseOutput | Where-Object { ($_.Trim().StartsWith("Tags:")) }; + + if (!($tagLine)) { + Write-Host "$logLead : No tags found for package '$name'"; + continue; + } + + $tagLine = $tagLine.Substring($tagLine.IndexOf(":") + 1); + $option = [System.StringSplitOptions]::RemoveEmptyEntries; + $tags = $tagLine.Split(' ', $option); + + if ($tags.count -eq 0) { + Write-Host "$logLead : No tags found for package '$name'"; + continue; + } + + Write-Host "$logLead : Found tags for package '$name': $tags"; + + $tagSet = New-Object System.Collections.Generic.HashSet[string]; + foreach ($tag in $tags) { + #ToLower() to get rid of tag ambiguity. + [void]$tagSet.Add($tag.ToLower()); + } + + $package.Tags = $tagSet; + } +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.ps1 new file mode 100644 index 0000000..de540f8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.ps1 @@ -0,0 +1,73 @@ +function Start-ServicesByTier { +<# +.SYNOPSIS + Starts all services that fall under the Tier parameter + +.PARAMETER Tier + Number specifing the tier of services + +.PARAMETER IncludeLowerTiers + Also include services from all tiers lower than the provided tier. + (With this switch present, -Tier 0 starts only tier 0 service names, 1 starts both tier 0 and 1 services, 2 starts + tier 2, 1, and 0, ad infinitum.) Values provided above the number of defined tiers simply start all available tiered services. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateRange(0, [int]::MaxValue)] + [int]$Tier, + + [Parameter(Mandatory = $false)] + [switch]$IncludeLowerTiers + ) + + $hostname = $env:COMPUTERNAME + $results = @{ + Hostname = $hostname + HasError = $false + ReturnValue = @() + } + + $maxTier = $Tier + if ($IncludeLowerTiers) { + $minTier = 0 + } else { + $minTier = $Tier + } + + # Assigning $minTier to 0 if the $IncludeLowerTiers param is enabled, otherwise matching up the $min/$maxTier params to only iterate through one tier + for ($i = $minTier; $i -le $maxTier; $i++) { + # Because I want to make my conditionals more readable + $isStartingTier0 = $i -eq 0 + $stoppedServicesInTier = @() + try { + $stoppedServicesInTier = @() + $servicesByTier = (Get-ServicesByTier -Tier $i) + foreach ($service in $servicesByTier) { + $serviceStatus = (Get-Service $service -ErrorAction SilentlyContinue).Status -eq "Stopped" + if ($serviceStatus) { + $stoppedServicesInTier += $service + } + } + $hasStoppedServicesInTier = -not (Test-IsCollectionNullOrEmpty -Collection $stoppedServicesInTier) + } catch { + Write-Warning $_ + } finally { + if ($hasStoppedServicesInTier) { + $startServicesResults += Start-ServicesInParallel -ServiceNamestoStart $stoppedServicesInTier -ReturnResults + + $results.ReturnValue = $startServicesResults + $results.HasError = -not (Test-IsCollectionNullOrEmpty -Collection $results.ReturnValue) + } else { + Write-Host "All services in Tier $i were already running" + $results.ReturnValue = @() + } + } + if ($results.HasError -and $isStartingTier0) { + # when Tier 0 (Subscription,Broker) fails to start, return results immediately so we can fail the job + Write-Warning "Exceptions were thrown trying to start serivces in Tier $i" + Write-Warning "Returning a list of Exceptions or ErrorRecords" + } + return $results + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.tests.ps1 new file mode 100644 index 0000000..f7acde8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Start-ServicesByTier.tests.ps1 @@ -0,0 +1,46 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-ServicesByTier" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServicesByTier -MockWith { return @("Fake.Service.One", "Fake.Service.Two") } + Mock -ModuleName $moduleForMock -CommandName Start-ServicesInParallel + Mock -ModuleName $moduleForMock -CommandName Test-IsCollectionNullOrEmpty -MockWith { return @("Fake.Service.One", "Fake.Service.Two") } + Mock -ModuleName $moduleForMock -CommandName Get-Service + Mock -ModuleName $moduleForMock -CommandName Write-Warning + Mock -ModuleName $moduleForMock -CommandName Write-Host + + Context "Start services by tier with different parameters" { + + It "Assert Get-ServicesByTier called with -IncludeLowerTiers" { + + Start-ServicesByTier -Tier 2 -IncludeLowerTiers + Assert-MockCalled -CommandName Get-ServicesByTier -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $IncludeLowerTiers -match $null } + } + + It "Assert Get-ServicesByTier called without -IncludeLowerTiers" { + + Start-ServicesByTier -Tier 2 + Assert-MockCalled -CommandName Get-ServicesByTier -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $IncludeLowerTiers -match $null } + } + + } + + Context "Start services by tier foreach logic output" { + + It "Assert Start-ServicesInParallel with Tier 0" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServicesByTier -MockWith { return @("Fake.Service.One") } + Mock -ModuleName $moduleForMock -CommandName Test-IsCollectionNullOrEmpty -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return @{ Status = "Stopped" } } + + Start-ServicesByTier -Tier 0 + Assert-MockCalled -CommandName Start-ServicesInParallel -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $ServiceNamestoStart -match "Fake.Service.One" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ChocoPackagesAvailable.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ChocoPackagesAvailable.ps1 new file mode 100644 index 0000000..1ded45c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ChocoPackagesAvailable.ps1 @@ -0,0 +1,17 @@ +function Test-ChocoPackagesAvailable { + <# +.SYNOPSIS + Verifies that a list of packages and their versions are available in the Chocolatey package feed. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$packagesText, + [pscredential]$NugetCredential + + ) + + $packageObjects = Get-ChocoStateReleaseInput($packagesText); + return (Test-ChocoPackagesAvailablePrivate -packages $packageObjects -NugetCredential $NugetCredential); +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsChocoPackageInstalled.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsChocoPackageInstalled.ps1 new file mode 100644 index 0000000..f63be01 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsChocoPackageInstalled.ps1 @@ -0,0 +1,19 @@ +function Test-IsChocoPackageInstalled { +<# +.SYNOPSIS + This function checks to see if a chocolatey package with a given id has been installed. + +.PARAMETER PackageName + [string] The name/id of a chocolatey package +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$PackageName + ) + + $chocoInstallPath = (Get-ChocolateyInstallPath) + $packagePath = (Join-Path (Join-Path $chocoInstallPath "lib") $PackageName) + + return (Test-Path $packagePath) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsEclairInstalled.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsEclairInstalled.ps1 new file mode 100644 index 0000000..5143737 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsEclairInstalled.ps1 @@ -0,0 +1,47 @@ +function Test-IsEclairInstalled { + <# + .SYNOPSIS + Returns true if the Alkami Eclair package manager is installed and running correctly. + + .DESCRIPTION + Eclair has the key "ALKAMI_ECLAIR" in the Description of the assembly. This can be found by + inspecting the VersionInfo object and getting the "Comments" field. + + .PARAMETER ComputerName + Remote computer to inspct for Eclair installation. Uses UNC share. + #> + + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = Get-logLeadName + $isEclairInstalled = $false + $chocoPath = (Get-Item (Join-Path (Get-ChocolateyInstallPath) 'choco.exe')) + + if($ComputerName -ne "localhost") { + # Get the UNC path and find the file. + if($ComputerName -notlike "*.fh.local") { + $ComputerName = "$ComputerName.fh.local" + } + Write-Host "$logLead : Checking $computerName for choco.exe" + + $chocoPath = Get-UncPath -ComputerName $ComputerName -filePath $chocoPath + } + + Write-Host "$logLead : Checking path $chocoPath for VersionInfo." + + $versionInfo = (Get-Item $chocoPath).VersionInfo + + $versionInfoJson = $versionInfo | ConvertTo-Json -Depth 6 + Write-Verbose "$logLead : $versionInfoJson" + + if($versionInfo.Comments -like "ALKAMI_ECLAIR*") { + $isEclairInstalled = $true + } + + return $isEclairInstalled +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageComponentizedWebApplication.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageComponentizedWebApplication.ps1 new file mode 100644 index 0000000..bfd3295 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageComponentizedWebApplication.ps1 @@ -0,0 +1,31 @@ +function Test-IsPackageComponentizedWebApplication { + <# +.SYNOPSIS + Returns true if a particular package is a legacy web application + that has been componentized into a chocolatey package. + +.PARAMETER PackageName + The package name to check + +.PARAMETER Package + The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({ if ($PSCmdlet.ParameterSetName -eq 'PackageName') { return -NOT (Test-IsStringNullOrWhiteSpace -Value $_) } else { return $true } })] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({ if ($PSCmdlet.ParameterSetName -eq 'Package') { return $null -ne $_ } else { return $true } })] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + $isComponentizedWebApp = $PackageName -in $_ComponentizedWebApplications + + return $isComponentizedWebApp +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbms.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbms.ps1 new file mode 100644 index 0000000..f27ab8c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbms.ps1 @@ -0,0 +1,40 @@ +function Test-IsPackageDbms { +<# +.SYNOPSIS + Returns true if the alkami microservice package needs to run with a database-accessible dbms account. +#> + [CmdletBinding(DefaultParameterSetName = 'FullPackage')] + Param( + [Parameter(Mandatory=$true, ParameterSetName='FullPackage')] + [string]$FeedSource, + [Parameter(Mandatory=$true, ParameterSetName='FullPackage')] + [string]$Name, + [Parameter(Mandatory=$true, ParameterSetName='FullPackage')] + [string]$Version, + [Alias("nuspec")] + [Parameter(Mandatory=$true, ParameterSetName='Nuspec')] + [xml]$NuspecXmlObject, + [Parameter(Mandatory=$false, ParameterSetName='Nuspec')] + [Parameter(Mandatory=$false, ParameterSetName='FullPackage')] + [PSCredential]$Credential = $null + ) + + $arguments = @{ + Dependency = $_DbmsDependencies # List of dbms package dependencies + Credential = $Credential + } + + if($NuspecXmlObject) { + $arguments += @{ + NuspecXmlObject = $NuspecXmlObject + } + } else { + $arguments += @{ + FeedSource = $FeedSource + Name = $Name + Version = $Version + } + } + + return Test-PackageHasDependency @arguments; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbmsV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbmsV2.ps1 new file mode 100644 index 0000000..e9ccded --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageDbmsV2.ps1 @@ -0,0 +1,22 @@ +function Test-IsPackageDbmsV2 { +<# +.SYNOPSIS + Returns true if the legacy microservice package needs to run with a database-accessible dbms account + +.PARAMETER NuspecXmlObject + The nuspec object to test against +#> + [CmdletBinding()] + Param( + [Alias("nuspec")] + [Parameter(Mandatory = $false)] # not required: for unit testing purposes because we test in a invoke-parallel we can't. Also it's not really super required. Something else would've failed first. + [object]$NuspecXmlObject + ) + + $arguments = @{ + DependencyList = $_DbmsDependencies + NuspecXmlObject = $NuspecXmlObject + } + + return Test-PackageHasDependencyV2 @arguments +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageFullScaleMicroserviceV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageFullScaleMicroserviceV2.ps1 new file mode 100644 index 0000000..83b53fc --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageFullScaleMicroserviceV2.ps1 @@ -0,0 +1,28 @@ +function Test-IsPackageFullScaleMicroserviceV2 { +<# +.SYNOPSIS + Returns true if a particular package is known as an full scale microservice + +.PARAMETER PackageName + [string] The package name to check + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'PackageName') {return ![string]::IsNullOrWhiteSpace($_)} else {return $true}})] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'Package') {return $null -ne $_} else {return $true}})] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + return ($_FullScaleMicroservicesAllowList.Where({$PackageName -like $_}).Count -gt 0) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInFeed.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInFeed.ps1 new file mode 100644 index 0000000..b3e94a7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInFeed.ps1 @@ -0,0 +1,35 @@ +function Test-IsPackageInFeed { +<# +.SYNOPSIS + Returns true if a particular name/version of a package is in a given nuget/chocolatey source feed. + +.PARAMETER Name + The package name + +.PARAMETER Source + The package source url + +.PARAMETER Version + The package version +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $false)] + [Alias("s")] + [string]$Source = "", + [Parameter(Mandatory = $false)] + [Alias("n")] + [string]$Name = "", + [Parameter(Mandatory = $false)] + [Alias("v")] + [string]$Version = "" + ) + + ## TODO: Would this be faster as a proget lookup? + ## TODO: Cleanup to match standards + ## TODO: Use Get-ChocoState + $package = (choco search "$Name" --version "$Version" -r -s "$Source") + + return ($null -ne $package) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInfrastructureMicroserviceV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInfrastructureMicroserviceV2.ps1 new file mode 100644 index 0000000..ff6e67f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInfrastructureMicroserviceV2.ps1 @@ -0,0 +1,28 @@ +function Test-IsPackageInfrastructureMicroserviceV2 { +<# +.SYNOPSIS + Returns true if a particular package is known as an infrastructure microservice + +.PARAMETER PackageName + [string] The package name to check + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'PackageName') {return ![string]::IsNullOrWhiteSpace($_)} else {return $true}})] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'Package') {return $null -ne $_} else {return $true}})] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + return ($_InfrastructureMicroservices.Where({$PackageName -like $_}).Where({$_}).Count -gt 0) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstaller.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstaller.ps1 new file mode 100644 index 0000000..ce5078e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstaller.ps1 @@ -0,0 +1,19 @@ +function Test-IsPackageInstaller { +<# +.SYNOPSIS + Returns true if a particular package is an installer package. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory=$true)] + [string]$packageName + ) + + foreach($match in $_InstallerPackages) { + if($packageName -like $match) { + return $true; + } + } + return $false; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstallerV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstallerV2.ps1 new file mode 100644 index 0000000..81e74cd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageInstallerV2.ps1 @@ -0,0 +1,28 @@ +function Test-IsPackageInstallerV2 { +<# +.SYNOPSIS + Returns true if a particular package is an installer package. + +.PARAMETER PackageName + [string] The package name to check + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'PackageName') {return ![string]::IsNullOrWhiteSpace($_)} else {return $true}})] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'Package') {return $null -ne $_} else {return $true}})] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + return ($_InstallerPackages.Where({$PackageName -like $_}).Where({$_}).Count -gt 0) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageMicroserviceV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageMicroserviceV2.ps1 new file mode 100644 index 0000000..17dbb07 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageMicroserviceV2.ps1 @@ -0,0 +1,60 @@ +function Test-IsPackageMicroserviceV2 { +<# +.SYNOPSIS + Returns true if the given package is a microservice. + +.PARAMETER NuspecXmlObject + The nuspec object to test against + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding()] + Param( + [Alias("nuspec")] + [Parameter(Mandatory = $false)] # not required: for unit testing purposes because we test in a invoke-parallel we can't. Also it's not really super required. Something else would've failed first. + [object]$NuspecXmlObject, + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [object]$Package + ) + + $loglead = (Get-LogLeadName) + + $name = $Package.Name + $version = $Package.Version + + # Return false if the package is an installer. + if(Test-IsPackageInstallerV2 -packageName $name) { + Write-Verbose "$logLead : Determined that package $name|$version is an installer package" + return $false + } + + # Are there any files that match the path pattern + # The closest Linq .Any() match is .Count -gt 0, so we do that + + # Return false on denylisted microservices. + if ($_MicroserviceDenyList.Where({$name -like $_}).Count -gt 0) { + Write-Verbose "$logLead : Determined that package $name|$version is universally denied as a service" + return $false + } + + # Return true on allowlisted microservices. + if ($_MicroserviceAllowList.Where({$name -like $_}).Count -gt 0) { + Write-Verbose "$logLead : Determined that package $name|$version is universally allowed as a service" + return $true + } + + $arguments = @{ + DependencyList = $_MicroDependencies + NuspecXmlObject = $NuspecXmlObject + } + + $isMicroservice = (Test-PackageHasDependencyV2 @arguments) + + # Write friendly verbose text. + $microText = if($isMicroservice) { "is" } else { "is not" } + Write-Verbose "$loglead : Determined that package $name|$version $microText a microservice" + + return $isMicroservice +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModule.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModule.ps1 new file mode 100644 index 0000000..b408143 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModule.ps1 @@ -0,0 +1,19 @@ +function Test-IsPackagePowerShellModule { +<# +.SYNOPSIS + Returns true if a particular package is considered a PowerShell module. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory=$true)] + [string]$packageName + ) + + foreach($match in $_PowerShellModulePackages) { + if($packageName -like $match) { + return $true; + } + } + return $false; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModuleV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModuleV2.ps1 new file mode 100644 index 0000000..21a65c4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackagePowerShellModuleV2.ps1 @@ -0,0 +1,28 @@ +function Test-IsPackagePowerShellModuleV2 { +<# +.SYNOPSIS + Returns true if a particular package is considered a PowerShell module. + +.PARAMETER PackageName + [string] The package name to check + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'PackageName') {return ![string]::IsNullOrWhiteSpace($_)} else {return $true}})] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'Package') {return $null -ne $_} else {return $true}})] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + return ($_PowerShellModulePackages.Where({$PackageName -match $_}).Count -gt 0) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.Tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.Tests.ps1 new file mode 100644 index 0000000..b4d7883 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.Tests.ps1 @@ -0,0 +1,73 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-IsPackageReliableService" { + + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + # Fake responses from Invoke-WebRequest + $validReliableServicePackage = "[{`"fullPath`":`"ApplicationManifest.xml`",`"parentFullPath`":`"/`",`"name`":`"ApplicationManifest.xml`",`"isDirectory`":false},{`"fullPath`":`"svc/ServiceManifest.xml`",`"parentFullPath`":`"svc/`",`"name`":`"ServiceManifest.xml`",`"isDirectory`":false}]" + $reliableServicePackageMissingServiceManifest = "[{`"fullPath`":`"ApplicationManifest.xml`",`"parentFullPath`":`"/`",`"name`":`"ApplicationManifest.xml`",`"isDirectory`":false}]" + $reliableServicePackageMissingApplicationManifest = "[{`"fullPath`":`"svc/ServiceManifest.xml`",`"parentFullPath`":`"svc/`",`"name`":`"ServiceManifest.xml`",`"isDirectory`":false}]" + $nonReliableServicePackage = "[{`"fullPath`":`"package/files/otherfile.xml`",`"parentFullPath`":`"package/files/`",`"name`":`"otherfile.xml`",`"isDirectory`":false}]" + + Context "Valid Reliable Service Package" { + + Mock -CommandName Get-PackageFileList -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $validReliableServicePackage) + } + It "Correctly Identifies a Reliable Service File List" { + $result = Test-IsPackageReliableService -feedSource $source -name $name -version $version; + $result | Should -be $true; + } + } + + Context "Incomplete Reliable Service Package - Missing Service Manifest" { + Mock -CommandName Get-PackageFileList -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $reliableServicePackageMissingServiceManifest) + } + It "Returns False Because it is Missing a Service Manifest" { + $result = Test-IsPackageReliableService -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + Context "Incomplete Reliable Service Package - Missing Application Manifest" { + Mock -CommandName Get-PackageFileList -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $reliableServicePackageMissingApplicationManifest) + } + It "Returns False Because it is Missing an Application Manifest" { + $result = Test-IsPackageReliableService -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + Context "Not A Reliable Service Package" { + Mock -CommandName Get-PackageFileList -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $nonReliableServicePackage) + } + It "Returns False Because it is not a Reliable Service Package" { + $result = Test-IsPackageReliableService -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + # This is an invalid test. "Get-PackageFileList" should be mocked + # This test should validate how this SUT responds when the return value of Get-PackageFileList is $null + # The _behavior_ of this original test should be moved to tests for "Get-PackageFileList" instead + <#Context "Bad Nuget Source" { + $source = "https://www.badpackagerepo.com/bad.feed"; + It "Fails Because the Source Feed URL is Badly Formatted" { + { Test-IsPackageReliableService -feedSource $source -name $name -version $version -ErrorAction Stop} | Should -Throw; + } + }#> +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.ps1 new file mode 100644 index 0000000..240b5eb --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableService.ps1 @@ -0,0 +1,57 @@ +function Test-IsPackageReliableService { +<# +.SYNOPSIS + Returns true if the given package is a Service Fabric Reliable Service. + A Reliable Service is a class library that implements Service Fabric's reliable service pattern, as opposed to a topshelf application. + If a package ships with Service Fabric Application/Service manifests it is assumed to be a reliable service. +#> + [CmdletBinding(DefaultParameterSetName = 'FeedQuery')] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$true, ParameterSetName='FeedQuery')] + [string]$FeedSource, + [Parameter(Mandatory=$true, ParameterSetName='FeedQuery')] + [string]$Name, + [Parameter(Mandatory=$true, ParameterSetName='FeedQuery')] + [string]$Version, + [Parameter(Mandatory=$false, ParameterSetName='FeedQuery')] + [PSCredential]$Credential = $null, + [Parameter(Mandatory=$true, ParameterSetName='ProvidedPackageFiles')] + [object]$PackageFiles + ) + + $loglead = (Get-LogLeadName); + + if($PSCmdlet.ParameterSetName -eq "FeedQuery") { + $PackageFiles = Get-PackageFileList -FeedSource $FeedSource -Name $Name -Version $Version -Credential $Credential; + } + + # Consider the package a reliable service if it contains both an application and a service manifest. + # Search for these two files in known locations. + + # Search for the application manifest. + $hasApplicationManifest = $false; + foreach ($file in $PackageFiles) { + if ($file.fullPath -eq "ApplicationManifest.xml") { + $hasApplicationManifest = $true; + break; + } + } + + # Search for the service manifest. + $hasServiceManifest = $false; + foreach ($file in $PackageFiles) { + if ($file.fullPath -eq "svc/ServiceManifest.xml") { + $hasServiceManifest = $true; + break; + } + } + + # Consider the package a reliable service if it contains the application/service manifests. + $isReliableService = $hasApplicationManifest -and $hasServiceManifest; + + $microText = if($isReliableService) { "is" } else { "is not" }; + Write-Verbose "$loglead : Determined that package $Name|$Version $microText a reliable service"; + + return $isReliableService; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.ps1 new file mode 100644 index 0000000..beea3ff --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.ps1 @@ -0,0 +1,79 @@ +function Test-IsPackageReliableServiceV2 { +<# +.SYNOPSIS + Returns true if the given package is a Service Fabric Reliable Service. + A Reliable Service is a class library that implements Service Fabric's reliable service pattern, as opposed to a topshelf application. + If a package ships with Service Fabric Application/Service manifests it is assumed to be a reliable service. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER PackageFiles + [object[]] List of packages from the Proget results + Array of objects have the shape: @{ fullpath=; parentFullPath=; name=; isDirectory; } + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName='RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory=$true, ParameterSetName='ProvidedPackageFiles')] + [object]$PackageFiles, + + [object]$Package, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'RawArgs') { + $Package = @{ + Feed = @{ + Source = $FeedSource + } + Name = $Name + Version = $Version + } + } else { + $Name = $Package.Name + $Version = $Package.Version + } + + if($PSCmdlet.ParameterSetName -ne 'ProvidedPackageFiles') { + $PackageFiles = Get-PackageFileListV2 -Package $Package -Credential $Credential + } + + # Consider the package a reliable service if it contains both an application and a service manifest. + # Search for these two files in known locations. + + $applicationManifestPattern = 'ApplicationManifest.xml' + $serviceManifestPattern = 'svc/ServiceManifest.xml' + + # Are there any files that match the path pattern + # The closest Linq .Any() match is .Count -gt 0, so we do that + $hasApplicationManifest = (@($PackageFiles.fullPath -like $applicationManifestPattern).Where({$_}).Count -gt 0) + $hasServiceManifest = (@($PackageFiles.fullPath -like $serviceManifestPattern).Where({$_}).Count -gt 0) + + # Consider the package a reliable service if it contains the application/service manifests. + $isReliableService = $hasApplicationManifest -and $hasServiceManifest; + + $microText = if($isReliableService) { "is" } else { "is not" }; + Write-Verbose "$loglead : Determined that package $Name|$Version $microText a reliable service"; + + return $isReliableService; +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.tests.ps1 new file mode 100644 index 0000000..0227494 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageReliableServiceV2.tests.ps1 @@ -0,0 +1,73 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-IsPackageReliableService" { + + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + # Fake responses from Invoke-WebRequest + $validReliableServicePackage = "[{`"fullPath`":`"ApplicationManifest.xml`",`"parentFullPath`":`"/`",`"name`":`"ApplicationManifest.xml`",`"isDirectory`":false},{`"fullPath`":`"svc/ServiceManifest.xml`",`"parentFullPath`":`"svc/`",`"name`":`"ServiceManifest.xml`",`"isDirectory`":false}]" + $reliableServicePackageMissingServiceManifest = "[{`"fullPath`":`"ApplicationManifest.xml`",`"parentFullPath`":`"/`",`"name`":`"ApplicationManifest.xml`",`"isDirectory`":false}]" + $reliableServicePackageMissingApplicationManifest = "[{`"fullPath`":`"svc/ServiceManifest.xml`",`"parentFullPath`":`"svc/`",`"name`":`"ServiceManifest.xml`",`"isDirectory`":false}]" + $nonReliableServicePackage = "[{`"fullPath`":`"package/files/otherfile.xml`",`"parentFullPath`":`"package/files/`",`"name`":`"otherfile.xml`",`"isDirectory`":false}]" + + Context "Valid Reliable Service Package" { + + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $validReliableServicePackage) + } + It "Correctly Identifies a Reliable Service File List" { + $result = Test-IsPackageReliableServiceV2 -feedSource $source -name $name -version $version; + $result | Should -be $true; + } + } + + Context "Incomplete Reliable Service Package - Missing Service Manifest" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $reliableServicePackageMissingServiceManifest) + } + It "Returns False Because it is Missing a Service Manifest" { + $result = Test-IsPackageReliableServiceV2 -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + Context "Incomplete Reliable Service Package - Missing Application Manifest" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $reliableServicePackageMissingApplicationManifest) + } + It "Returns False Because it is Missing an Application Manifest" { + $result = Test-IsPackageReliableServiceV2 -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + Context "Not A Reliable Service Package" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $nonReliableServicePackage) + } + It "Returns False Because it is not a Reliable Service Package" { + $result = Test-IsPackageReliableServiceV2 -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + # This is an invalid test. "Get-PackageFileList" should be mocked + # This test should validate how this SUT responds when the return value of Get-PackageFileList is $null + # The _behavior_ of this original test should be moved to tests for "Get-PackageFileList" instead + <#Context "Bad Nuget Source" { + $source = "https://www.badpackagerepo.com/bad.feed"; + It "Fails Because the Source Feed URL is Badly Formatted" { + { Test-IsPackageReliableService -feedSource $source -name $name -version $version -ErrorAction Stop} | Should -Throw; + } + }#> +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageUpgradeOnlyV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageUpgradeOnlyV2.ps1 new file mode 100644 index 0000000..ad68992 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsPackageUpgradeOnlyV2.ps1 @@ -0,0 +1,28 @@ +function Test-IsPackageUpgradeOnlyV2 { +<# +.SYNOPSIS + Returns true if a particular package should -only- be run in upgrade mode, and never force reinstalled. + +.PARAMETER PackageName + [string] The package name to check + +.PARAMETER Package + [object] The package to check +#> + [CmdletBinding(DefaultParameterSetName = 'PackageName')] + [OutputType([bool])] + Param ( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'PackageName')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'PackageName') {return ![string]::IsNullOrWhiteSpace($_)} else {return $true}})] + [string]$PackageName, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Package')] + [ValidateScript({if ($PSCmdlet.ParameterSetName -eq 'Package') {return $null -ne $_} else {return $true}})] + [object]$Package + ) + + if ($PSCmdlet.ParameterSetName -eq 'Package') { + $PackageName = $Package.Name + } + + return ($_UpgradePackages.Where({$PackageName -like $_}).Where({$_}).Count -gt 0) +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.ps1 new file mode 100644 index 0000000..5642c39 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.ps1 @@ -0,0 +1,25 @@ +function Test-IsServiceManifestCore { + <# + .SYNOPSIS + Used by Set-DotNetCoreProfiling to determine if the package ia a dot net core service + + .PARAMETER ServiceManifest + [object] childnode or equivalent json dotted child + + .EXAMPLE + $packageManifest = Get-PackageManifest -Path $Directory + if ((Test-IsServiceManifestCore $packageManifest ) -eq $true ) + #> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [object]$ServiceManifest + ) + + if ($ServiceManifest.ServiceManifest.runtime -in @("dotnetcore", "core")) { + return $true + } else { + return $false + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.tests.ps1 new file mode 100644 index 0000000..1a18e4c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-IsServiceManifestCore.tests.ps1 @@ -0,0 +1,38 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-IsServiceManifestCore" { + + $global:manifestCore = @" + + + + _replaceME_ + + +"@ + + Context "manifest is not null" { + It "runtime is dotnetcore" { + $manifest = $manifestCore.Replace("_replaceME_", "dotnetcore") + $packageManifest = (([xml]($manifest.Clone())).SelectNodes('//packageManifest')) + Test-IsServiceManifestCore -ServiceManifest $packageManifest | Should -BeTrue + } + It "runtime is core" { + $manifest = $manifestCore.Replace("_replaceME_", "core") + $packageManifest = (([xml]($manifest.Clone())).SelectNodes('//packageManifest')) + Test-IsServiceManifestCore -ServiceManifest $packageManifest | Should -BeTrue + } + It "runtime is not core" { + $manifest = $manifestCore.Replace("_replaceME_", "blarg") + $packageManifest = (([xml]($manifest.Clone())).SelectNodes('//packageManifest')) + Test-IsServiceManifestCore -ServiceManifest $packageManifest | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ManualChocoCommandsExecuted.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ManualChocoCommandsExecuted.ps1 new file mode 100644 index 0000000..ef4a1fd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ManualChocoCommandsExecuted.ps1 @@ -0,0 +1,28 @@ +function Test-ManualChocoCommandsExecuted { +<# +.SYNOPSIS + Verifies that the packages installed through the build-server were successfully installed. + Checks that Install-ManualChocoPackages was executed, and the command file was renamed. + +.PARAMETER CommandsPath + [string] Indicate where the chocoInstallCommands file is located +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$false)] + [string]$CommandsPath = "C:/temp/deploy/chocoInstallCommands.ps1" + ) + + $logLead = (Get-LogLeadName) + + # Make sure that the manual package install command was run. + if(!(Test-Path $CommandsPath)) { + Write-Host "$logLead Chocolatey install script $CommandsPath was not found. This is acceptable." + return $true + } else { + Write-Error "$logLead The chocolatey install script at $CommandsPath was not executed and renamed. Run Install-ManualChocoPackages, or rename the file if you know what you are doing." + return $false + } +} + diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.ps1 new file mode 100644 index 0000000..02ff63d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.ps1 @@ -0,0 +1,69 @@ +function Test-PackageHasAlkamiManifestV2 { +<# +.SYNOPSIS + Returns true if the given package contains an Alkami manifest. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER PackageFiles + [object[]] List of packages from the Proget results + Array of objects have the shape: @{ fullpath=; parentFullPath=; name=; isDirectory; } + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName='RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory=$true, ParameterSetName='ProvidedPackageFiles')] + [object]$PackageFiles, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'RawArgs') { + $Package = @{ + Feed = @{ + Source = $FeedSource + } + Name = $Name + Version = $Version + } + } else { + $Name = $Package.Name + $Version = $Package.Version + } + + if($PSCmdlet.ParameterSetName -ne 'ProvidedPackageFiles') { + Write-Host "$logLead : No package list provided, fetching from remote server" + $PackageFiles = Get-PackageFileListV2 -Package $Package -Credential $Credential + } + + foreach ($filename in (Get-ValidPackageManifestFilenames)) { + # The .name property is always just the rightmost filename part of the record + if ($PackageFiles.Where({$_.name -eq $filename -and !$_.fullPath.StartsWith("src/")}).Where({$_}).Count -gt 0) { + return $true + } + } + + return $false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.tests.ps1 new file mode 100644 index 0000000..c6ad556 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasAlkamiManifestV2.tests.ps1 @@ -0,0 +1,190 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-PackageHasAlkamiManifest" { + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + # Fake responses from Invoke-WebRequest for mocked Get-PackageFileList + $validAlkamiManifestPackage = "[{`"fullPath`":`"tools/Whatever.dll`",`"parentFullPath`":`"tools/`",`"name`":`"Whatever.dll`",`"isDirectory`":false},{`"fullPath`":`"AlkamiManifest.xml`",`"parentFullPath`":`"/`",`"name`":`"AlkamiManifest.xml`",`"isDirectory`":false}]" + $invalidAlkamiManifestPackage = "[{`"fullPath`":`"tools/Whatever.dll`",`"parentFullPath`":`"tools/`",`"name`":`"Whatever.dll`",`"isDirectory`":false},{`"fullPath`":`"src/AlkamiManifest.xml`",`"parentFullPath`":`"/src`",`"name`":`"AlkamiManifest.xml`",`"isDirectory`":false}]" + $missingAlkamiManifest = "[{`"fullPath`":`"package/files/otherfile.xml`",`"parentFullPath`":`"package/files/`",`"name`":`"otherfile.xml`",`"isDirectory`":false}]" + + Context "Valid Alkami Manifest Package" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $validAlkamiManifestPackage) + } + + It "Correctly Identifies an Alkami Manifest" { + (Test-PackageHasAlkamiManifestV2 -feedSource $source -name $name -version $version) | Should -BeTrue + } + } + + Context "Invalid Alkami Manifest Package" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $invalidAlkamiManifestPackage) + } + + It "Correctly Identifies an Alkami Manifest" { + (Test-PackageHasAlkamiManifestV2 -feedSource $source -name $name -version $version) | Should -BeFalse + } + } + + Context "Not a Package With an Alkami Manifest" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return @(ConvertFrom-Json -InputObject $missingAlkamiManifest) + } + It "Returns False Because it is not an Alkami Manifest Package" { + (Test-PackageHasAlkamiManifestV2 -feedSource $source -name $name -version $version) | Should -BeFalse + } + } + +} + +# Ok, this next test block is very weird. Basically, I'm trying to say "if the other file doesn't do what it should then this one should still do what it should" +# I'm probably doing this wrong and we should 100% revisit in 2022 and ask if we are doing the right thing +# TODO: Revisit this test strategy to ensure we need it. This is an attempt at properly testing SRE-17072 - cbrand 2021-12-15 +Describe "Test-PackageHasAlkamiManifest_with_Integration" -Tags @('Integration') { + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + Context "Uses the pattern from Get-PackageFileListV2 test to do an integration test" { + $additionalSut = "Get-PackageFileListV2.ps1" + $functionPath = Join-Path -Path $here -ChildPath $additionalSut + Write-Host "Overriding Additional-SUT: $functionPath" + Import-Module $functionPath -Force + + $script:testString = @" +[ + { + "fullPath": "src/VCU.MS.CardControl.Notifications/", + "parentFullPath": "src/", + "name": "VCU.MS.CardControl.Notifications", + "isDirectory": true + }, + { + "fullPath": "src/", + "parentFullPath": "/", + "name": "src", + "isDirectory": true + }, + { + "fullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/", + "parentFullPath": "src/VCU.MS.CardControl.Notifications/", + "name": "VCU.Modules.CardControl", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/core-properties/", + "parentFullPath": "package/services/metadata/", + "name": "core-properties", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/", + "parentFullPath": "package/services/", + "name": "metadata", + "isDirectory": true + }, + { + "fullPath": "package/services/", + "parentFullPath": "package/", + "name": "services", + "isDirectory": true + }, + { + "fullPath": "package/", + "parentFullPath": "/", + "name": "package", + "isDirectory": true + }, + { + "fullPath": "package/services/metadata/core-properties/5bb485eb5b744639ac99ff7821cf325a.psmdcp", + "parentFullPath": "package/services/metadata/core-properties/", + "name": "5bb485eb5b744639ac99ff7821cf325a.psmdcp", + "isDirectory": false + }, + { + "fullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/AlkamiManifest.xml", + "parentFullPath": "src/VCU.MS.CardControl.Notifications/VCU.Modules.CardControl/", + "name": "AlkamiManifest.xml", + "isDirectory": false + }, + { + "fullPath": "_rels/", + "parentFullPath": "/", + "name": "_rels", + "isDirectory": true + }, + { + "fullPath": "_rels/.rels", + "parentFullPath": "_rels/", + "name": ".rels", + "isDirectory": false + }, + { + "fullPath": "tools/", + "parentFullPath": "/", + "name": "tools", + "isDirectory": true + }, + { + "fullPath": "tools/VCU.MS.CardControl.Notifications.Host.exe", + "parentFullPath": "tools/", + "name": "VCU.MS.CardControl.Notifications.Host.exe", + "isDirectory": false + }, + { + "fullPath": "tools/ChocolateyUninstall.ps1", + "parentFullPath": "tools/", + "name": "ChocolateyUninstall.ps1", + "isDirectory": false + }, + { + "fullPath": "tools/ChocolateyInstall.ps1", + "parentFullPath": "tools/", + "name": "ChocolateyInstall.ps1", + "isDirectory": false + }, + { + "fullPath": "VCU.MS.CardControl.Notifications.Host.nuspec", + "parentFullPath": "/", + "name": "VCU.MS.CardControl.Notifications.Host.nuspec", + "isDirectory": false + }, + { + "fullPath": "[Content_Types].xml", + "parentFullPath": "/", + "name": "[Content_Types].xml", + "isDirectory": false + } +] +"@ + Mock -Module $moduleForMock -CommandName Invoke-ProgetRequest -MockWith { return $script:testString } + Mock -Module $moduleForMock -CommandName Get-BasicAuthHeader -MockWith { return @{} } # just give an empty object for parameter + + It "Returns False Because it is not an Alkami Manifest Package" { + (Test-PackageHasAlkamiManifestV2 -feedSource $source -name $name -version $version) | Should -BeFalse + } + } + + # This is an invalid test. "Get-PackageFileList" should be mocked + # This test should validate how this SUT responds when the return value of Get-PackageFileList is $null + # The _behavior_ of this original test should be moved to tests for "Get-PackageFileList" instead + <#Context "Bad Nuget Source" { + $source = "https://www.badpackagerepo.com/bad.feed"; + It "Fails Because the Source Feed URL is Badly Formatted" { + { Test-PackageHasAlkamiManifest -feedSource $source -name $name -version $version -ErrorAction Stop} | Should -Throw; + } + }#> +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.ps1 new file mode 100644 index 0000000..2c5ff9b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.ps1 @@ -0,0 +1,70 @@ +function Test-PackageHasDatabaseConfigFile { + <# +.SYNOPSIS + Returns true if the given package contains a DatabaseConfig.ps1 file. + +.PARAMETER FeedSource + Source feed used to look up the package by + +.PARAMETER Name + Package name to lookup + +.PARAMETER Version + Package version to lookup + +.PARAMETER PackageFiles + List of packages from previously retrieved Proget results + Array of objects have the shape: @( { fullpath=; parentFullPath=; name=; isDirectory; } ) + +.PARAMETER Credential + Credential used for accessing feeds, as needed +#> + [CmdletBinding(DefaultParameterSetName = 'RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + + [Parameter(Mandatory = $true, ParameterSetName = 'ProvidedPackageFiles')] + [object]$PackageFiles, + + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + $loglead = Get-LogLeadName + + + if ($PSCmdlet.ParameterSetName -ne 'ProvidedPackageFiles') { + if ($PSCmdlet.ParameterSetName -eq 'RawArgs') { + $Package = @{ + Feed = @{ + Source = $FeedSource + } + Name = $Name + Version = $Version + } + } + Write-Host "$logLead : No package list provided, fetching from remote server" + $PackageFiles = Get-PackageFileListV2 -Package $Package -Credential $Credential + } + + $validDatabaseConfigFilenames = Get-ValidPackageDatabaseConfigFilenames + + foreach ($filename in $validDatabaseConfigFilenames) { + # The .name property is always just the rightmost filename part of the record + if ($PackageFiles.Where({ + $_.name -eq $filename -and !$_.fullPath.StartsWith("src/") + }).Where({ $_ }).Count -gt 0) { + return $true + } + } + + return $false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.tests.ps1 new file mode 100644 index 0000000..695c605 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDatabaseConfigFile.tests.ps1 @@ -0,0 +1,53 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-PackageHasDatabaseConfigFile" { + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + # Fake responses from Invoke-WebRequest for mocked Get-PackageFileList + $validDatabaseConfigPackage = "[{`"fullPath`":`"tools/Whatever.dll`",`"parentFullPath`":`"tools/`",`"name`":`"Whatever.dll`",`"isDirectory`":false},{`"fullPath`":`"DatabaseConfig.ps1`",`"parentFullPath`":`"/`",`"name`":`"DatabaseConfig.ps1`",`"isDirectory`":false}]" + $invalidDatabaseConfigPackage = "[{`"fullPath`":`"tools/Whatever.dll`",`"parentFullPath`":`"tools/`",`"name`":`"Whatever.dll`",`"isDirectory`":false},{`"fullPath`":`"src/DatabaseConfig.ps1`",`"parentFullPath`":`"/src`",`"name`":`"DatabaseConfig.ps1`",`"isDirectory`":false}]" + $missingDatabaseConfigPackage = "[{`"fullPath`":`"package/files/otherfile.xml`",`"parentFullPath`":`"package/files/`",`"name`":`"otherfile.xml`",`"isDirectory`":false}]" + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith {} + + Context "Package Has Database Config In Valid Path With Valid Name" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $validDatabaseConfigPackage) + } + + It "Correctly Identifies Package Has a Database Config In Valid Path With Valid Name" { + (Test-PackageHasDatabaseConfigFile -feedSource $source -Name $name -version $version) | Should -BeTrue + } + } + + Context "Package Has A Database Config That Is Not In Valid Path With Valid Name" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $invalidDatabaseConfigPackage) + } + + It "Correctly Identifies Package Has A Database Config Not In Valid Path With Valid Name" { + (Test-PackageHasDatabaseConfigFile -feedSource $source -Name $name -version $version) | Should -BeFalse + } + } + + Context "Not a Package With a Database Config" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return @(ConvertFrom-Json -InputObject $missingDatabaseConfigPackage) + } + It "Returns False Because Package Does Not Have A Database Config" { + (Test-PackageHasDatabaseConfigFile -feedSource $source -Name $name -version $version) | Should -BeFalse + } + } + +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDependencyV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDependencyV2.ps1 new file mode 100644 index 0000000..e24cf1e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasDependencyV2.ps1 @@ -0,0 +1,36 @@ +function Test-PackageHasDependencyV2 { +<# +.SYNOPSIS + Returns true if the supplied nuspec file has any of the given dependencies + +.PARAMETER NuspecXmlObject + [xml] A nuspec object + +.PARAMETER DependencyList + [string[]] List of dependencies +#> + [CmdletBinding()] + Param( + [Alias("nuspec")] + [Parameter(Mandatory = $false)] # not required: for unit testing purposes because we test in a invoke-parallel we can't. Also it's not really super required. Something else would've failed first. + [object]$NuspecXmlObject, + [string[]]$DependencyList + ) + + # Get dependencies of the package + $nuspecDependencies = @($NuspecXmlObject.package.metadata.dependencies.ChildNodes | Select-Object -ExpandProperty id) + + # This is the more typical case + if ($nuspecDependencies.Count -eq 0) { + return $false + } + + # Figure out if the package has any of the $Dependency dependencies. + foreach($dependency in @($DependencyList)) { + if($nuspecDependencies -contains $dependency) { + return $true + } + } + + return $false +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.ps1 new file mode 100644 index 0000000..7402c1c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.ps1 @@ -0,0 +1,62 @@ +function Test-PackageHasInfrastructureMigrationsV2 { +<# +.SYNOPSIS + Returns true if the given package contains Terraform infrastructure migrations. + +.PARAMETER FeedSource + [string] Source feed used to look up the package by + +.PARAMETER Name + [string] Package name to lookup + +.PARAMETER Version + [string] Package version to lookup + +.PARAMETER PackageFiles + [object[]] List of packages from the Proget results + Array of objects have the shape: @{ fullpath=; parentFullPath=; name=; isDirectory; } + +.PARAMETER Credential + [PSCredential] Credential used for talking to feeds as needed +#> + [CmdletBinding(DefaultParameterSetName='RawArgs')] + Param( + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$FeedSource, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'RawArgs')] + [string]$Version, + [Parameter(Mandatory = $true, ParameterSetName = 'Package')] + [object]$Package, + [Parameter(Mandatory=$true, ParameterSetName='ProvidedPackageFiles')] + [object]$PackageFiles, + [Parameter(Mandatory = $false)] + [PSCredential]$Credential = $null + ) + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'RawArgs') { + $Package = @{ + Feed = @{ + Source = $FeedSource + } + Name = $Name + Version = $Version + } + } else { + $Name = $Package.Name + $Version = $Package.Version + } + + if($PSCmdlet.ParameterSetName -ne 'ProvidedPackageFiles') { + Write-Host "$logLead : No package list provided, fetching from remote server" + $PackageFiles = Get-PackageFileListV2 -Package $Package -Credential $Credential + } + + # Are there any files that match the path pattern + # The closest Linq .Any() match is .Count -gt 0, so we do that + $terraformPathPattern = 'terraform/*' + return (@($PackageFiles.fullPath -like $terraformPathPattern).Where({$_}).Count -gt 0) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.tests.ps1 new file mode 100644 index 0000000..f4199c5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-PackageHasInfrastructureMigrationsV2.tests.ps1 @@ -0,0 +1,52 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Test-PackageHasInfrastructureMigrations" { + + # Describe fake package info. + $source = "https://www.fakepackagerepo.com/nuget/fake.feed"; + $name = "fake.package"; + $version = "1.2.3"; + + # Fake responses from Invoke-WebRequest for mocked Get-PackageFileList + $validInfraMigrationPackage = "[{`"fullPath`":`"tools/Whatever.dll`",`"parentFullPath`":`"tools/`",`"name`":`"Whatever.dll`",`"isDirectory`":false},{`"fullPath`":`"Terraform/main.tf`",`"parentFullPath`":`"/`",`"name`":`"main.tf`",`"isDirectory`":false}]" + $nonInfraMigrationPackage = "[{`"fullPath`":`"package/files/otherfile.xml`",`"parentFullPath`":`"package/files/`",`"name`":`"otherfile.xml`",`"isDirectory`":false}]" + + + Context "Valid Infrastructure Migration Package" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $validInfraMigrationPackage) + } + + It "Correctly Identifies an Infrastructure Migration Microservice" { + $result = Test-PackageHasInfrastructureMigrationsV2 -feedSource $source -name $name -version $version; + $result | Should -be $true; + } + } + + Context "Not an Infrastructure Migration Package" { + Mock -CommandName Get-PackageFileListV2 -ModuleName $moduleForMock -MockWith { + return (ConvertFrom-Json -InputObject $nonInfraMigrationPackage) + } + It "Returns False Because it is not an Infrastructure Migration Package" { + $result = Test-PackageHasInfrastructureMigrationsV2 -feedSource $source -name $name -version $version; + $result | Should -be $false; + } + } + + # This is an invalid test. "Get-PackageFileList" should be mocked + # This test should validate how this SUT responds when the return value of Get-PackageFileList is $null + # The _behavior_ of this original test should be moved to tests for "Get-PackageFileList" instead + <#Context "Bad Nuget Source" { + $source = "https://www.badpackagerepo.com/bad.feed"; + It "Fails Because the Source Feed URL is Badly Formatted" { + { Test-PackageHasInfrastructureMigrations -feedSource $source -name $name -version $version -ErrorAction Stop} | Should -Throw; + } + }#> +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.ps1 new file mode 100644 index 0000000..33e5e61 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.ps1 @@ -0,0 +1,38 @@ +function Test-ServiceManifestHasMigrations { +<# +.SYNOPSIS + Used by both categorization and installer package to denote that the manifest requires db access + Using it in both places allows us to change/update the logic if we need to in the future. + +.PARAMETER ServiceManifest + [object] childnode or equivalent json dotted child +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [object]$ServiceManifest + ) + + $logLead = Get-LogLeadName + + # this lets the categorizer pass in an empty or null node to reduce complexity of testing + # Additionally, a bad value clearly means we don't need to check with the database + if ($null -eq $ServiceManifest) { + return $false + } + + # These are necessary for running any migrations etc + $migrationsAssemblies = @($ServiceManifest.migrations.assembly).Where({$null -ne $_}) + $migrationsPackages = @($ServiceManifest.migrations.package).Where({$null -ne $_}) + + # We can have Assemblies or Packages, but not both. + $hasMigrationAssemblies = (!(Test-IsCollectionNullOrEmpty -Collection $migrationsAssemblies)) + $hasMigrationPackages = (!(Test-IsCollectionNullOrEmpty -Collection $migrationsPackages)) + $hasMigrations = $hasMigrationAssemblies -or $hasMigrationPackages + + Write-Host "$logLead : Service has migration assemblies [$hasMigrationAssemblies]" + Write-Host "$logLead : Service has migration packages [$hasMigrationPackages]" + Write-Host "$logLead : Service has migrations [$hasMigrations] <-- returned value" + + return $hasMigrations +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.tests.ps1 new file mode 100644 index 0000000..b55db03 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestHasMigrations.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-ServiceManifestHasMigrations" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + + Context "When Service Manifest Is Null" { + $serviceManifest = $null + + It "Returns False"{ + $results = Test-ServiceManifestHasMigrations $serviceManifest + + $results | Should -BeFalse + } + } + + Context "When Service Manifest Has Assemblies But No Packages" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Assembly = @{ + role = "" + } + } + } + + It "Returns True"{ + $results = Test-ServiceManifestHasMigrations $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Packages But No Assemblies" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Package = @{ + } + } + } + + It "Returns True"{ + $results = Test-ServiceManifestHasMigrations $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Neither Packages Nor Assemblies" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + } + } + It "Returns False"{ + $results = Test-ServiceManifestHasMigrations $serviceManifest + + $results | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.ps1 new file mode 100644 index 0000000..56a8b8e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.ps1 @@ -0,0 +1,53 @@ +function Test-ServiceManifestRequiresDbAccess { +<# +.SYNOPSIS + Used by both categorization and installer package to denote that the manifest requires db access + Using it in both places allows us to change/update the logic if we need to in the future. + +.PARAMETER ServiceManifest + [object] childnode or equivalent json dotted child +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [object]$ServiceManifest + ) + + $logLead = Get-LogLeadName + + # this lets the categorizer pass in an empty or null node to reduce complexity of testing + # Additionally, a bad value clearly means we don't need to check with the database + if ($null -eq $ServiceManifest) { + return $false + } + + # These are necessary for running any migrations etc + $migrationsAssemblies = @($ServiceManifest.migrations.assembly).Where({$null -ne $_}) + $migrationsPackages = @($ServiceManifest.migrations.package).Where({$null -ne $_}) + $dbRole = $ServiceManifest.db_role + + $hasMigrationAssemblies = (!(Test-IsCollectionNullOrEmpty -Collection $migrationsAssemblies)) + $hasMigrationPackages = (!(Test-IsCollectionNullOrEmpty -Collection $migrationsPackages)) + $hasDbRole = ![string]::IsNullOrWhiteSpace($dbRole) + $hasAssemblyRole = $false + foreach($assembly in $migrationsAssemblies){ + if(!(Test-StringIsNullOrWhiteSpace($assembly.role))){ + $hasAssemblyRole = $true + break + } + } + + # If there is a db role, or an assembly role, or packages then there's no database access required + if ($hasDbRole -or $hasAssemblyRole -or $hasMigrationPackages) { + $databaseAccessRequired = $true + } else { + $databaseAccessRequired = $false + } + + Write-Host "$logLead : Service has migration assemblies [$hasMigrationAssemblies]" + Write-Host "$logLead : Service has migration packages [$hasMigrationPackages]" + Write-Host "$logLead : Service has dbRole [$hasDbRole]" + Write-Host "$logLead : Service requires database access [$databaseAccessRequired] <-- returned value" + + return $databaseAccessRequired +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.tests.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.tests.ps1 new file mode 100644 index 0000000..ac2586b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Test-ServiceManifestRequiresDbAccess.tests.ps1 @@ -0,0 +1,155 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-ServiceManifestRequiresDbAccess" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + + Context "When Service Manifest Is Null" { + $serviceManifest = $null + + It "Returns False"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeFalse + } + } + + Context "When Service Manifest Has Assemblies With A Role But No DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Assembly = @() + } + } + + $serviceManifest.Migrations.Assembly += @{ + role = "MyAssemblyRole" + } + + $serviceManifest.Migrations.Assembly += @{ + role = "MyOtherAssemblyRole" + } + + It "Returns True"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has One or More Assemblies With A Role and One or More without But No DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Assembly = @() + } + } + + $serviceManifest.Migrations.Assembly += @{ + role = "MyAssemblyRole" + } + + $serviceManifest.Migrations.Assembly += @{ + role = "" + } + + It "Returns True"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Assemblies Without A Role And No DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Assembly = @() + } + } + + $serviceManifest.Migrations.Assembly += @{ + role = "" + } + + $serviceManifest.Migrations.Assembly += @{ + role = "" + } + + It "Returns False"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeFalse + } + } + + Context "When Service Manifest Has Packages But No DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Package = @{ + } + } + } + + It "Returns True"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Assemblies And A DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Assembly = @{ + role = "" + } + } + db_role = "MyRole" + } + + It "Returns True"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Packages And A DB Role" { + $serviceManifest = @{ + Runtime = "Core" + Migrations = @{ + Package = @{ + } + } + db_role = "MyRole" + } + + It "Returns True"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeTrue + } + } + + Context "When Service Manifest Has Neither Packages Nor Assemblies Nor A DB Role" { + $serviceManifest = @{ + Runtime = "Core" + } + + It "Returns False"{ + $results = Test-ServiceManifestRequiresDbAccess $serviceManifest + + $results | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/Public/Update-FeedAuthentication.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Update-FeedAuthentication.ps1 new file mode 100644 index 0000000..341e29b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Update-FeedAuthentication.ps1 @@ -0,0 +1,52 @@ +function Update-FeedAuthentication { +<# +.SYNOPSIS + Update feed authentication for choco feeds. + +.DESCRIPTION + Updates the user authentication stored in choco for a given feed. + +.PARAMETER Username + [string] The username to use for all sources + +.PARAMETER Password + [string] Plain text value password +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Username = "AlkamiSDKSubscriber", + + [Parameter(Mandatory = $true)] + [string]$Password + ) + + $logLead = (Get-LogLeadName) + + $chocoSources = (Get-ChocolateySources) + foreach($chocoSource in $chocoSources) { + if($chocoSource.isSDK) { + $name = $chocoSource.Name + $source = $chocoSource.Source + $displayPassword = $password.Substring(0,4) + + Write-Host "$logLead : Resetting feed with provided information for $name [$source]" + + Write-Verbose "$logLead : choco source remove -n=`"$name`"" + choco source remove -n="$name" + + Write-Verbose "$logLead : choco source add -n=`"$name`" -s=`"$source`" -u=`"$Username`" -p=`"$displayPassword`"" + choco source add -n="$name" -s="$source" -u="$Username" -p="$Password" + + Write-Verbose "$logLead : added back $name" + $validationCheckResponses = choco list -s $source --page-size 1 --page 0 + + ## Using 401 as the match for: The remote server returned an error: (401) Unauthorized. + if (!!($validationCheckResponses -match '401')) { + Write-Error "$logLead : The username and password provided returned an Unauthorized Failure for $name [$source]" + } else { + Write-Verbose "$logLead : successfully tested $name" + } + } + } +} diff --git a/Modules/Alkami.PowerShell.Choco/Public/Write-InstallPackageMetadataToConsole.ps1 b/Modules/Alkami.PowerShell.Choco/Public/Write-InstallPackageMetadataToConsole.ps1 new file mode 100644 index 0000000..883dcbc --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/Public/Write-InstallPackageMetadataToConsole.ps1 @@ -0,0 +1,42 @@ +function Write-InstallPackageMetadataToConsole { + <# + .SYNOPSIS + Dumps package metadata to console. + .PARAMETER Packages + List of packages to dump. + .PARAMETER IsWeb + Switch determining if it's a web deploy or not. + #> + [CmdletBinding()] + param( + [object[]]$Packages, + [switch]$IsWeb + ) + + foreach ($package in $Packages) { + Write-Host ("##teamcity[blockOpened name='$($package.Name) $($package.Version)']") + Write-Host "Classifications:" + Write-Host "`tTier: $($package.Tier)" + Write-Host "`tMicroservice: $($package.IsMicroservice)" + Write-Host "`tInstaller: $($package.IsInstaller)" + Write-Host "`tInfrastructure: $($package.IsInfrastructure)" + Write-Host "`tHas Migrations: $($package.HasMigrations)" + Write-Host "`tIs MigrationOnlyPackage: $($package.IsMigrationPackage)" + Write-Host "`tHas Infrastructure Migrations: $($package.HasInfrastructureMigration)" + Write-Host "`tSDK: $($package.IsSDK)" + + # Doctor the web/app dichotomy of installs a bit because of how the web/app install boxes work. + # Without this all of the app packages will say InstallToWeb True. + $installToWeb = $isWeb.IsPresent -and $package.InstallToWeb + $installToApp = $package.InstallToApp + $installToMic = $package.InstallToMic + $installToFab = $package.InstallToFab + + Write-Host "`nInstall To Servers:" + Write-Host "`tWebs: $($installToWeb)" + Write-Host "`tApps: $($installToApp)" + Write-Host "`tMics: $($installToMic)" + Write-Host "`tFabs: $($installToFab)" + Write-Host ("##teamcity[blockClosed name='$($package.Name)']") + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/Nonsense folder/There is nothing here.txt b/Modules/Alkami.PowerShell.Choco/TestFiles/Nonsense folder/There is nothing here.txt new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.API.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.API.json new file mode 100644 index 0000000..1b12a00 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.API.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.7.0-pre006", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Admin.API", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.CardManagement.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.CardManagement.json new file mode 100644 index 0000000..e95584c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.CardManagement.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.7.3-pre051", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Admin.CardManagement", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.Operations.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.Operations.json new file mode 100644 index 0000000..8fcad60 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.Operations.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "4.8.1", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Admin.Widgets.Operations", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.RetailACH.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.RetailACH.json new file mode 100644 index 0000000..c58ef85 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Admin.Widgets.RetailACH.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.6.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Admin.Widgets.RetailACH", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.AFX.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.AFX.json new file mode 100644 index 0000000..0da20dd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.AFX.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.30.5-pre181", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Api.AFX", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.Alexa.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.Alexa.json new file mode 100644 index 0000000..cc82ffd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.Alexa.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.26", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Api.Alexa", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.CUFX.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.CUFX.json new file mode 100644 index 0000000..8ac9366 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Api.CUFX.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.31.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Api.CUFX", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Nag.Providers.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Nag.Providers.Json new file mode 100644 index 0000000..be5e5ef --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Nag.Providers.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Nag.Providers", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Processor.Wire.FedwireOutput.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Processor.Wire.FedwireOutput.Json new file mode 100644 index 0000000..c8d7304 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Processor.Wire.FedwireOutput.Json @@ -0,0 +1,52 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.6.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Processor.Wire.FedwireOutput", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage" : false, + "IsHotfix" : false, + "SkipUninstallScripts": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Catalyst.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Catalyst.json new file mode 100644 index 0000000..948a9ec --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Catalyst.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.3", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.CheckImaging.Catalyst", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.CorporateOne.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.CorporateOne.Json new file mode 100644 index 0000000..6a7192f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.CorporateOne.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.1", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.CheckImaging.CorporateOne", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Easycard.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Easycard.json new file mode 100644 index 0000000..a382e2a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.CheckImaging.Easycard.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.CheckImaging.Easycard", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Multiplexer.Client.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Multiplexer.Client.Json new file mode 100644 index 0000000..48656ac --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Multiplexer.Client.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.Multiplexer.Client", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Radium.BillPay.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Radium.BillPay.Json new file mode 100644 index 0000000..a3ef513 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Providers.Radium.BillPay.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.Radium.BillPay", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Radium.Dispatcher.Akka.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Radium.Dispatcher.Akka.json new file mode 100644 index 0000000..218eec5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.App.Radium.Dispatcher.Akka.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Radium.Dispatcher.Akka", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.AugeoRewards.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.AugeoRewards.json new file mode 100644 index 0000000..5c8e749 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.AugeoRewards.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.7", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Apps.AugeoRewards", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.Authentication.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.Authentication.json new file mode 100644 index 0000000..dcd7edb --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Apps.Authentication.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.19.0-pre513", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Apps.Authentication", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AccountLinking.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AccountLinking.json new file mode 100644 index 0000000..1172c40 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AccountLinking.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.5.0-pre094", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Client.Widget.AccountLinking", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AtLogin.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AtLogin.json new file mode 100644 index 0000000..5bf29e5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Client.Widget.AtLogin.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.4.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Client.Widget.AtLogin", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Dummy.SRE.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Dummy.SRE.json new file mode 100644 index 0000000..b220733 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Dummy.SRE.json @@ -0,0 +1,57 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "HasMigrations": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "IsDbms": false, + "StartMode": null, + "ReinstallWithORB": true, + "IsInfrastructure": false, + "Version": "2023.5.1", + "NewRelicAppName": null, + "IsComponentizedWebApp": false, + "IsInstaller": false, + "HotfixFixedInOrbVersion": "2023.04.0.1", + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed/", + "IsDefault": false, + "Ordering": 1, + "Priority": 0, + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": true, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Installer", + "Hotfix" + ], + "ComponentType": "Hotfix", + "InstallToAppTier": true, + "IsReportPackage": false, + "InstallToApp": true, + "InstallToFab": false, + "ServerTier": "ALL", + "IsMissingFromServers": null, + "Name": "Alkami.Dummy.SRE", + "SkipUninstallScripts": false, + "Tier": 2 +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Provider.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Provider.Json new file mode 100644 index 0000000..8a66f76 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Provider.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.Provider", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.WebExtension.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.WebExtension.Json new file mode 100644 index 0000000..37598d2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.WebExtension.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.1", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.WebExtension", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Widget.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Widget.Json new file mode 100644 index 0000000..a01f16a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Installer.Widget.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.2", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.Widget", + "SkipUninstallScripts": false, + "Tier": -1, + "IsReportPackage" : false, + "HasMigrations": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.ScheduleTransactionSync.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.ScheduleTransactionSync.json new file mode 100644 index 0000000..4888c65 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.ScheduleTransactionSync.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Legacy.Sync.ScheduleTransactionSync", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.Transactions.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.Transactions.json new file mode 100644 index 0000000..c4c7974 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Legacy.Sync.Transactions.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.8.2", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Legacy.Sync.Transactions", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.AccountsOrchestration.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.AccountsOrchestration.Service.Host.json new file mode 100644 index 0000000..c7b886f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.AccountsOrchestration.Service.Host.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.2-pre061", + "NewRelicAppName": "Alkami.MS.AccountsOrchestration", + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MS.AccountsOrchestration.Service.Host", + "Tier": -1, + "EnableNewRelic": true, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.ActionableAlertsOrchestration.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.ActionableAlertsOrchestration.Service.Host.json new file mode 100644 index 0000000..098ac6f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MS.ActionableAlertsOrchestration.Service.Host.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.0", + "NewRelicAppName": "Alkami.MS.ActionableAlertsOrchestration", + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MS.ActionableAlertsOrchestration.Service.Host", + "Tier": -1, + "EnableNewRelic": false, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Audit.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Audit.Service.Host.Json new file mode 100644 index 0000000..badd655 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Audit.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "6.11.0", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Audit.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Authorization.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Authorization.Service.Host.Json new file mode 100644 index 0000000..8ecb3ca --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Authorization.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.4.3", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Authorization.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.AutoBiller.Symitar.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.AutoBiller.Symitar.Service.Host.Json new file mode 100644 index 0000000..6db584b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.AutoBiller.Symitar.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.4", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.AutoBiller.Symitar.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Broker.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Broker.Host.Json new file mode 100644 index 0000000..842852d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Broker.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": true, + "Version": "2.8.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Broker.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CardManagementProviders.SymConnect.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CardManagementProviders.SymConnect.Host.Json new file mode 100644 index 0000000..5901992 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CardManagementProviders.SymConnect.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.7.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.CardManagementProviders.SymConnect.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Database.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Database.json new file mode 100644 index 0000000..8d4ddcd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Database.json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Choco.Installer.Database", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Logic.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Logic.json new file mode 100644 index 0000000..b4507e1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.Logic.json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Choco.Installer.Logic", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.MasterDatabase.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.MasterDatabase.Json new file mode 100644 index 0000000..b640a0b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Choco.Installer.MasterDatabase.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Choco.Installer.MasterDatabase", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CivicRewards.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CivicRewards.Service.Host.json new file mode 100644 index 0000000..5542ee3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.CivicRewards.Service.Host.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.0.7", + "NewRelicAppName": "CivicRewards", + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.CivicRewards.Service.Host", + "Tier": -1, + "EnableNewRelic": false, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Contacts.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Contacts.Service.Host.Json new file mode 100644 index 0000000..5cb6e64 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Contacts.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.8", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Contacts.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.EventManagement.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.EventManagement.Service.Host.Json new file mode 100644 index 0000000..5e87f23 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.EventManagement.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.4.9", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.EventManagement.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Forms.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Forms.Service.Host.Json new file mode 100644 index 0000000..fd1d398 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Forms.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Forms.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Holidays.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Holidays.Service.Host.Json new file mode 100644 index 0000000..b69db1b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Holidays.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.12", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Holidays.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Images.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Images.Service.Host.Json new file mode 100644 index 0000000..3cfa7cd --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Images.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.1.14", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Images.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Notifications.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Notifications.Service.Host.Json new file mode 100644 index 0000000..60f914d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Notifications.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.6.12", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Notifications.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.QuickApply.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.QuickApply.Service.Host.Json new file mode 100644 index 0000000..6e41b1a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.QuickApply.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "9.0.0", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.QuickApply.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host.Json new file mode 100644 index 0000000..4bd6282 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.3", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "IsReportPackage" : false, + "HasMigrations": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Registration.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Registration.Service.Host.Json new file mode 100644 index 0000000..13da0c3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Registration.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.5.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Registration.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RemoteDeposit.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RemoteDeposit.Service.Host.Json new file mode 100644 index 0000000..cd64b6f --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.RemoteDeposit.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.RemoteDeposit.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Security.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Security.Service.Host.Json new file mode 100644 index 0000000..7c7a907 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Security.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.17.2", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Security.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Settings.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Settings.Service.Host.Json new file mode 100644 index 0000000..cf8f0db --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Settings.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "4.4.0", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Settings.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SiteText.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SiteText.Service.Host.Json new file mode 100644 index 0000000..f809a3a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SiteText.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.17", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.SiteText.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SymConnectMultiplexer.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SymConnectMultiplexer.Service.Host.Json new file mode 100644 index 0000000..159df28 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.SymConnectMultiplexer.Service.Host.Json @@ -0,0 +1,56 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.2", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "HasMigrations" : false, + "IsReportPackage" : false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.SymConnectMultiplexer.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Transactions.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Transactions.Service.Host.Json new file mode 100644 index 0000000..df3227d --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.Transactions.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.7.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Transactions.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.UserInterface.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.UserInterface.Service.Host.Json new file mode 100644 index 0000000..c69a81a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.UserInterface.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.17.0", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.UserInterface.Service.Host", + "SkipUninstallScripts": false, + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.VisaGiveBack.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.VisaGiveBack.Service.Host.json new file mode 100644 index 0000000..2ae9546 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.MicroServices.VisaGiveBack.Service.Host.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.6", + "NewRelicAppName": "Alkami.MicroServices.VisaGiveBack.Service", + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.VisaGiveBack.Service.Host", + "Tier": -1, + "EnableNewRelic": false, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Migrations.UserReporting.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Migrations.UserReporting.Service.Host.json new file mode 100644 index 0000000..2335309 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Migrations.UserReporting.Service.Host.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.2.8", + "NewRelicAppName": "Alkami.Migrations.UserReporting", + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Migrations.UserReporting.Service.Host", + "Tier": -1, + "EnableNewRelic": false, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.ACHDisclosure.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.ACHDisclosure.json new file mode 100644 index 0000000..bf94a3a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.ACHDisclosure.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.2.2", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Modules.ACHDisclosure", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.AtLogin.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.AtLogin.json new file mode 100644 index 0000000..8eeeb08 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Modules.AtLogin.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.4.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Modules.AtLogin", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Ops.Common.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Ops.Common.Json new file mode 100644 index 0000000..cd122b8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Ops.Common.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.3", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Ops.Common", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Choco.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Choco.Json new file mode 100644 index 0000000..cfb0d1e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Choco.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.5.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.PowerShell.Choco", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Common.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Common.Json new file mode 100644 index 0000000..d605771 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.PowerShell.Common.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.2.6", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.PowerShell.Common", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Session.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Session.json new file mode 100644 index 0000000..2951129 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Session.json @@ -0,0 +1,51 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.2.2", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Services.Session", + "Tier": -1, + "EnableNewRelic": false, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Subscriptions.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Subscriptions.Host.json new file mode 100644 index 0000000..b99c74e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Services.Subscriptions.Host.json @@ -0,0 +1,52 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": true, + "Version": "3.9.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Services.Subscriptions.Host", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage" : false, + "SkipUninstallScripts" : false, + "IsHotfix": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Utilities.NagViewer.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Utilities.NagViewer.json new file mode 100644 index 0000000..d82b802 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.Utilities.NagViewer.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.1", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": true, + "IsService": null, + "ForceInstallToAppDetermination": true, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": true, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Utilities.NagViewer", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApp.VisaNotificationCallback.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApp.VisaNotificationCallback.json new file mode 100644 index 0000000..2231106 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApp.VisaNotificationCallback.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.12", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.WebApp.VisaNotificationCallback", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApps.Isotope.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApps.Isotope.json new file mode 100644 index 0000000..887bf1c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/Alkami.WebApps.Isotope.json @@ -0,0 +1,50 @@ +{ + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": false, + "InstallToWeb": true, + "HasAlkamiManifest": true, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.6.0-pre173", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": false, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.WebApps.Isotope", + "Tier": -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.AccountService.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.AccountService.MS.Service.Host.Json new file mode 100644 index 0000000..1f0804e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.AccountService.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.9", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.AccountService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.BranchService.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.BranchService.MS.Service.Host.Json new file mode 100644 index 0000000..9c00967 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.BranchService.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.10", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.BranchService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CMN.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CMN.MS.Service.Host.Json new file mode 100644 index 0000000..c9b2e81 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CMN.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CMN.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CopyRequestService.MS.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CopyRequestService.MS.Host.Json new file mode 100644 index 0000000..2329396 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CopyRequestService.MS.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.1", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CopyRequestService.MS.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CoreService.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CoreService.MS.Service.Host.Json new file mode 100644 index 0000000..4c3a97a --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.CoreService.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.0.5", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CoreService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.EStatements.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.EStatements.MS.Service.Host.Json new file mode 100644 index 0000000..e8fa68b --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.EStatements.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.7", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.EStatements.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MarketingEmailService.MS.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MarketingEmailService.MS.Host.Json new file mode 100644 index 0000000..d2c7451 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MarketingEmailService.MS.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.7", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.MarketingEmailService.MS.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MicroService.ContentService.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MicroService.ContentService.Service.Host.Json new file mode 100644 index 0000000..dfe4502 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.MicroService.ContentService.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.0.4", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.MicroService.ContentService.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SecureMessage.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SecureMessage.MS.Service.Host.Json new file mode 100644 index 0000000..a68f745 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SecureMessage.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.10", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.SecureMessage.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.Settings.MS.Service.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.Settings.MS.Service.Host.Json new file mode 100644 index 0000000..0eb8de1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.Settings.MS.Service.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.Settings.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SkipAPayService.MS.Host.Json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SkipAPayService.MS.Host.Json new file mode 100644 index 0000000..6d4b810 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/DS.SkipAPayService.MS.Host.Json @@ -0,0 +1,54 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": "FakeNRAppName", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.SkipAPayService.MS.Host", + "SkipUninstallScripts": false, + "Tier" : -1, + "HasMigrations" : false, + "IsReportPackage": false, + "IsComponentizedWebApp": false +} diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host.json b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host.json new file mode 100644 index 0000000..cae699c --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/PackageObjects/fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host.json @@ -0,0 +1,49 @@ +{ + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.9.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "ForceInstallToAppTierDetermination": false, + "HasInfrastructureMigration": false, + "InstallToMicTier": true, + "ForceInstallToWebDetermination": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/fake.feed", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "Tags": [ + "FakeTag", + "OtherFakeTag" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "fake.Alkami.MicroServices.SymConnectMultiplexer.Service.Host", + "Tier": -1, + "EnableNewRelic": false, + "IsComponentizedWebApp": false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/TestProvider/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Choco/TestFiles/TestProvider/AlkamiManifest.xml new file mode 100644 index 0000000..791aea9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/TestProvider/AlkamiManifest.xml @@ -0,0 +1,18 @@ + + + 1.0 + + TEST + TestProvider + Provider + + + + + All + rootNamespace + TestProvider + TestProvider + Connector + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/TestWidget/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Choco/TestFiles/TestWidget/AlkamiManifest.xml new file mode 100644 index 0000000..46d6334 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/TestWidget/AlkamiManifest.xml @@ -0,0 +1,18 @@ + + + 1.0 + + TEST + TestWidget + Widget + + + This is a displayable widget name for the UI + This is a displayable widget description for the UI + Client + areaName + rootNamespace + Desktop + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/Write-OrderedJson.ps1 b/Modules/Alkami.PowerShell.Choco/TestFiles/Write-OrderedJson.ps1 new file mode 100644 index 0000000..c0f55d7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/Write-OrderedJson.ps1 @@ -0,0 +1,183 @@ +function Write-OrderedJson { + <# + .SYNOPSIS + Given a PSCustomObject, write it as pretty indented json WITH ORDERING for consistency + + .PARAMETER JsonObject + An object that should be written as json. Should be a PSCustomObject or Hashtable + + .PARAMETER InputPath + A path to a given file to reduce need to import file contents prior to running + + .PARAMETER NoKeyReorder + [switch] Should avoid reordering keys + + .PARAMETER Path + A filesystem path + #> + [CmdletBinding(DefaultParameterSetName = 'JsonObject')] + [OutputType([string])] + param ( + # Setting to mandatory false because it can be null and that's ok, we just return null + [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'JsonObject')] + $JsonObject, + [Parameter(Mandatory = $true, ParameterSetName = 'InputPath')] + $InputPath, + [switch]$NoKeyReorder, + [Parameter(Mandatory = $false)] + $Path + ) + + if ($PSCmdlet.ParameterSetName -eq 'InputPath') { + $JsonObject = (Get-Content -Path $InputPath -Raw) + } + + if ($JsonObject -is [string]) { + $JsonObject = (ConvertFrom-Json $JsonObject) + } elseif ($JsonObject -is [Array]) { + $JsonObject = (ConvertFrom-Json ([string]::Join('',$JsonObject))) + } + +#region innerFunctions + $stringBuilder = New-Object System.Text.StringBuilder + $quoteString = '"' + $commaString = "," + $OrderedKeys = !$NoKeyReorder + $primitiveMatch = 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean' + + function Get-JsonStringLeadsByDepth { + param ( + $Depth = 0 + ) + + $spacesString = [string]::new(" ", ($Depth + 1) * 2) + $shortSpacesString = "" + if ($Depth -gt 0) { + $shortSpacesString = [string]::new(" ", $Depth * 2) + } + + return ($spacesString, $shortSpacesString) + } + + function Build-JsonArrayNameValuePair { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name,Key,Id + } + + $stringBuilder.Append($shortSpacesString) | Out-Null + + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null + } + $stringBuilder.AppendLine("[") | Out-Null + + if (($JsonValue[0].GetType().Name -match $primitiveMatch) -or ($JsonValue[0] -is [string])) { + $JsonValue = $JsonValue | Sort-Object + } + + foreach ($iter in $JsonValue) { + if ($iter.GetType().Name -match $primitiveMatch) { + # Write it without quotes + $stringBuilder.AppendLine("$spacesString$iter$commaString") | Out-Null + } elseif ($iter -is [string]) { + # Write it with quotes + $stringBuilder.AppendLine("$spacesString$quoteString$iter$quoteString$commaString") | Out-Null + } else { + # Must be a complex object, but without a name, so write the value + Build-JsonObject -JsonName $null -JsonValue $iter -Depth ($Depth + 1) + } + } + $stringBuilder.AppendLine("$shortSpacesString]$commaString") | Out-Null + } + + function Build-JsonObject { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + $stringBuilder.Append($shortSpacesString) | Out-Null + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null + } + $stringBuilder.AppendLine("{") | Out-Null + if (!(Test-IsCollectionNullOrEmpty $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}))) { + $JsonValue = $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}) + } + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name, Key, Id + } + $keys = $JsonValue.Keys + if (Test-IsCollectionNullOrEmpty $keys) { + $keys = $JsonValue.Name + } + foreach ($key in $keys) { + $iter = $JsonValue[$key] + if ($null -eq $iter) { + $posit = $JsonValue.Where({$_.Name -eq $key}) + if ($null -ne $posit) { + $iter = $posit.Value + } + } + if ($null -eq $iter) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : null$commaString") | Out-Null + } else { + if ($iter -ceq "False") { $iter = $false } + if ($iter -ceq "True") { $iter = $true } + if ($iter.GetType().Name -match $primitiveMatch) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $($iter.ToString().ToLower())$commaString") | Out-Null + } elseif ($iter -is [string]) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $quoteString$iter$quoteString$commaString") | Out-Null + } elseif ($iter.GetType().IsArray) { + # values are an array, so let's write the array values + Build-JsonArrayNameValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } else { + # It was not an array, or a primitive, so it must be _another_ object? + Build-JsonObject -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } + } + } + $stringBuilder.AppendLine("$shortSpacesString}$commaString") | Out-Null + } +#endregion innerFunctions + +#region primitives should just be written as-is + if ($JsonObject -is [string]) { + return "$quoteString$JsonObject$quoteString" + } elseif ($JsonObject -is [bool]) { + return $JsonObject.ToString().ToLower() + } elseif ($JsonObject.GetType().Name -match $primitiveMatch) { + return $JsonObject.ToString() +#endregion primitives should just be written as-is + } elseif ($JsonObject.GetType().IsArray) { + Build-JsonArrayNameValuePair -JsonName $null -JsonValue $JsonObject + } else { + Build-JsonObject -JsonName $null -JsonValue $JsonObject + } + + # postprocess to convert this string: ",(\r?\n?\s*?[\]\}])" to this string "$1" (mind the escaping tho) + # That string says "any comma followed by any newline character(s) and any number of spaces, followed by a closing brace (indicating the end of an array or object) should remove the comma but retain the rest of the string" + # This removes trailing commas in arrays and object notations + $regex = New-Object System.Text.RegularExpressions.Regex(",(\r?\n?\s*?[\]\}])") + $builtString = $stringBuilder.ToString() + $builtString = $regex.Replace($builtString, "`$1") + $builtString = $builtString.TrimEnd().TrimEnd(",") + + if ([string]::IsNullOrWhiteSpace($Path)) { + Write-Host "Path: $path" + return $builtString + } else { + Set-Content -Path $Path -Value $builtString -Force | Out-Null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/data.json b/Modules/Alkami.PowerShell.Choco/TestFiles/data.json new file mode 100644 index 0000000..14bfb84 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/data.json @@ -0,0 +1,2293 @@ +[ + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Installer", + "Provider" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.Provider", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.1", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Installer", + "WebExtension" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.WebExtension", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.2", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Installer", + "Widget" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Installer.Widget", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.6", + "NewRelicAppName": null, + "IsInstaller": true, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": true, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Choco.Installer.MasterDatabase", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": true, + "Version": "3.5.2", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Services.Subscriptions.Host", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": true, + "Version": "2.8.1", + "NewRelicAppName": "Alkami.MicroServices.Broker", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Broker.Host", + "SkipUninstallScripts": false, + "Tier": 0 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.12", + "NewRelicAppName": "Alkami.MicroServices.Holidays.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Holidays.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.1.14", + "NewRelicAppName": "Alkami.MicroServices.Images.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Images.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "6.11.0", + "NewRelicAppName": "Alkami.MicroServices.Audit", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Audit.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.8", + "NewRelicAppName": "Alkami.MicroServices.Contacts.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Contacts.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.6.12", + "NewRelicAppName": "Alkami.MicroServices.Notifications", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Notifications.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.7.1", + "NewRelicAppName": "Alkami.MicroServices.Transactions", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Transactions.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.4.9", + "NewRelicAppName": "Alkami.MicroServices.EventManagement.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.EventManagement.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "4.4.0", + "NewRelicAppName": "Alkami.MicroServices.Settings.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Settings.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.17", + "NewRelicAppName": "Alkami.MicroServices.SiteText", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.SiteText.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.4.3", + "NewRelicAppName": "Alkami.MicroServices.Authorization", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": true, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Authorization.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.1", + "NewRelicAppName": "Alkami.MicroServices.Forms", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Forms.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.17.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.UserInterface.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.17.2", + "NewRelicAppName": "Alkami.MicroServices.Security.Service.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "Microservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Security.Service.Host", + "SkipUninstallScripts": false, + "Tier": 1 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.0.5", + "NewRelicAppName": "DS.CoreService.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CoreService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.7", + "NewRelicAppName": "DS.EStatements.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.EStatements.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.7", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.MarketingEmailService.MS.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.10", + "NewRelicAppName": "DS.BranchService.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.BranchService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": "DS.CMNService.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CMN.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.1", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DS.CopyRequestService.MS.Host" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.CopyRequestService.MS.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.SkipAPayService.MS.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.4", + "NewRelicAppName": "Alkami.MicroServices.AutoBiller.Static", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.AutoBiller.Symitar.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.3", + "NewRelicAppName": "Alkami.MicroServices.RDCoreDepositProviders.Symitar", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.0.4", + "NewRelicAppName": "DS.MicroService.ContentService", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.MicroService.ContentService.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.10", + "NewRelicAppName": "DS.SecureMessage.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.SecureMessage.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.8", + "NewRelicAppName": "DS.Settings.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.Settings.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.9", + "NewRelicAppName": "DS.AccountService.MS", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "SDK", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/sdk.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "DSFCU", + "SDK", + "MicroService" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "DS.AccountService.MS.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.7.1", + "NewRelicAppName": "Alkami.MicroServices.CardManagementProviders.SymConnect.Host", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": false, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.CardManagementProviders.SymConnect.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.4.1", + "NewRelicAppName": "Alkami.MicroServices.RemoteDeposit", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.RemoteDeposit.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.5.1", + "NewRelicAppName": "Alkami.MicroServices.Registration", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.Registration.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "2.1.2", + "NewRelicAppName": "Alkami.MicroServices.SymConnectMultiplexer", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice", + "ORB", + "SymConnect", + "Multiplexer" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.SymConnectMultiplexer.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": true, + "ForceSameVersion": false, + "InstallToApp": false, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": true, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "9.0.0", + "NewRelicAppName": "Alkami.MicroServices.QuickApply", + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "ConfigurableMicroservice" + ], + "IsDbms": true, + "InstallToAppTier": false, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.MicroServices.QuickApply.Service.Host", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "ORB", + "Core", + "Provider", + "SymConnect", + "Multiplexer" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.Multiplexer.Client", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.0.1", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "ORB", + "Check", + "Imaging", + "CheckImaging", + "Provider" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.CheckImaging.CorporateOne", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.5.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "PowerShell" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.PowerShell.Choco", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.0", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "billpay", + "provider", + "radium" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Providers.Radium.BillPay", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.2.6", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + "PowerShell" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.PowerShell.Common", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": false, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": true, + "InstallToMic": true, + "InstallToWeb": true, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": true, + "StartMode": null, + "IsInfrastructure": false, + "Version": "3.0.3", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "nuget.internal", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/nuget.internal", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": true, + "Tags": [ + + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.Ops.Common", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.2.3", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "processor", + "componentized", + "wire", + "fedwire", + "provider" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Processor.Wire.FedwireOutput", + "SkipUninstallScripts": false, + "Tier": 2 + }, + { + "IsSDK": false, + "IsMicroservice": false, + "ForceSameVersion": true, + "InstallToApp": true, + "IsMigrationPackage": false, + "InstallToWebTier": false, + "InstallToMic": false, + "InstallToWeb": false, + "HasAlkamiManifest": false, + "ForceInstallToAppTierDetermination": false, + "IsValid": true, + "Upgrade": false, + "StartMode": null, + "IsInfrastructure": false, + "Version": "1.1.4", + "NewRelicAppName": null, + "IsInstaller": false, + "MissingFromServers": [ + + ], + "ForceInstallToWebTierDetermination": false, + "IsService": null, + "ForceInstallToAppDetermination": false, + "PreventRollback": false, + "IsReliableService": false, + "IsFullScaleMicroservice": false, + "Feed": { + "Name": "Alkami", + "Source": "https://packagerepo.orb.alkamitech.com/nuget/choco.prod", + "IsDefault": false, + "Priority": "0", + "Disabled": false, + "IsSDK": false + }, + "IsHotfix": false, + "HasInfrastructureMigration": false, + "ForceInstallToWebDetermination": false, + "InstallToMicTier": false, + "Tags": [ + "provider", + "nag" + ], + "IsDbms": false, + "InstallToAppTier": true, + "InstallToFab": false, + "IsMissingFromServers": null, + "Name": "Alkami.App.Nag.Providers", + "SkipUninstallScripts": false, + "Tier": 2 + } +] diff --git a/Modules/Alkami.PowerShell.Choco/TestFiles/readme.md b/Modules/Alkami.PowerShell.Choco/TestFiles/readme.md new file mode 100644 index 0000000..701a712 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/TestFiles/readme.md @@ -0,0 +1,8 @@ +# Package Objects +This is a collection of json represtentations of packages. This is a subset of the same data you'd find in the PackageData.json file created by the ClassifyPackages deployment step. + +# Local Usage +These files are used by "Get-PackageInstallationData.Tests.ps1. They are used to simulate the output objects from Get-PackageMetadataV2. + +# Usage in TDC +The Classify Packages unit tests need these same objects. It will pull a copy of whatever files you have here locally when you run those tests. If these files are modified in any way, the corresponding PackageData-Expected-(Full/Minimal).json files must also be updated, or you'll have failing tests in that repo. \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.Choco/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Choco/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.Choco/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.Choco/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.nuspec b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.nuspec new file mode 100644 index 0000000..25ae205 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.nuspec @@ -0,0 +1,27 @@ + + + + Alkami.PowerShell.Common + $version$ + Alkami Platform Modules - PowerShell - Common + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.Common + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Common module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.psd1 b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.psd1 new file mode 100644 index 0000000..05bad2b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.psd1 @@ -0,0 +1,12 @@ +@{ + RootModule = 'Alkami.PowerShell.Common.psm1' + ModuleVersion = '3.29.1' + GUID = 'dcd8341b-19f4-4146-99de-93d68c06a91b' + Author = 'SRE' + CompanyName = '' + Copyright = '(c) 2018 Alkami. All rights reserved.' + Description = 'Alkami Common PowerShell functionality' + PowerShellVersion = '5.0' + FunctionsToExport = 'Add-DirectoryToPath','Add-HostsFileContent','Add-HostsFileEntry','Backup-AlkamiLogs','Backup-AlkamiOrb','Backup-LogFiles','Backup-ORBLogFiles','Close-SMBApplicationLocks','Compare-MetadataPart','Compare-ReleaseMetadata','Compare-SemVer','Compare-StringToLocalMachineIdentifiers','ConvertFrom-JsonToHashtable','ConvertFrom-Xml','ConvertTo-SafeTeamCityMessage','Copy-ObjectProperties','Find-CertificateByName','Format-Json','Format-Url','Get-7ZipPath','Get-AlkamiCredential','Get-AlkamiInstallationDrive','Get-AlkamiManifestFilename','Get-AvailabilityZone','Get-AwsSettings','Get-ChocolateyInstallPath','Get-CoalescedStringValue','Get-ConnectionString','Get-CPUUsage','Get-CurrentInstance','Get-CurrentInstanceAvailabilityZone','Get-CurrentInstanceId','Get-CurrentInstanceRegion','Get-CurrentInstanceTags','Get-DefaultLog4NetPathForPackage','Get-DotNetConfigPath','Get-DotNetVersion','Get-EnvironmentVariable','Get-FileEncoding','Get-FilesNoSymlink','Get-FilteredStringArray','Get-FolderSizeMb','Get-FullyQualifiedServerName','Get-HostsFileContent','Get-ImdsBaseUri','Get-ImdsV2Token','Get-InstanceMetadata','Get-InstanceTags','Get-IpAddress','Get-IPAddressesForName','Get-LogColor','Get-LogLeadName','Get-MachineConfigAppSetting','Get-MachineKeyDecryptionKey','Get-MachineKeyValidationKey','Get-MasterConnectionString','Get-NewRelicApmUpgradeData','Get-OrbLogsPath','Get-OrbPath','Get-OrbSharedPath','Get-OrbVersion','Get-ParentExecutionName','Get-ParentExecutionNameExampleUsage','Get-PasswordFromCredential','Get-SecretServerUri','Get-SecureString','Get-SecurityPolicy','Get-SecurityPolicySetting','Get-ServerByType','Get-ServerTypeByHostname','Get-SetDifference','Get-Sha256Hash','Get-SidFromUsername','Get-SupportedPlatformAPMVersion','Get-SupportedPlatformAPMVersionMap','Get-TempOrbDeployPath','Get-TextEditorPath','Get-UncPath','Get-UsernameFromSid','Get-UsersPath','Get-ValidatedRuntimeParameter','Get-VersionPSObject','Grant-RightsToFolderOrFile','Grant-UserLocalSecurityPolicyRights','Grant-UserProfileSystemPerformanceRights','Import-AWSModule','Import-TeamCityModule','Invoke-CallOperatorWithPathAndParameters','Invoke-CommandWithRetry','Invoke-Parallel','Invoke-Parallel2','Invoke-ParallelServers','Invoke-QueryOnClientDatabase','Move-LogsAndDeleteDotNetTemps','New-7Zip','New-AlkamiEventSource','New-AlkamiModule','New-DynamoMessageStringValue','New-SNSMessageAttribute','New-StatusIoIncident','New-Symlink','Open-UrlInDefaultBrowser','Out-FileWithRetry','Read-MachineConfig','Read-XMLFile','Remove-DotNetTemporaryFiles','Remove-FileSystemItem','Remove-OldArchivedLogFiles','Remove-ORBLogFiles','Resolve-Error','Revoke-LogonUsers','Save-XMLFile','Search-ForRunningWorkerProcesses','Select-AlkamiAppServers','Select-AlkamiFabServers','Select-AlkamiMicServers','Select-AlkamiTeaServers','Select-AlkamiWebServers','Set-ConnectionString','Set-RegistryValue','Set-SystemWebSettings','Set-XmlNodeValue','Stop-ProcessIfFound','Test-ComputerIsAvailable','Test-IsAdmin','Test-IsCollectionNullOrEmpty','Test-IsLoadTestEnvironment','Test-IsNull','Test-IsPsModuleInstalled','Test-IsStringIPAddress','Test-IsSymlink','Test-IsSymlinkValid','Test-PathIsInApprovedPackageLocation','Test-PathMatch','Test-PathsAreEqual','Test-ShouldContinue','Test-ShouldProcess','Test-StringIsNullOrEmpty','Test-StringIsNullOrWhitespace','Use-Module','Wait-ServersAreReachable','Write-ArrayToOutput' + AliasesToExport = 'AsDynamoMessageValue','AsSNSAttribute','Clean-Url','IsAdmin','IsCollectionNullOrEmpty','IsNull','Tee-OutFile' +} diff --git a/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.pssproj b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.pssproj new file mode 100644 index 0000000..e15ce75 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Alkami.PowerShell.Common.pssproj @@ -0,0 +1,177 @@ + + + Debug + 2.0 + {61968e62-3368-4743-95e5-5270102432fc} + Exe + MyApplication + MyApplication + Alkami.PowerShell.Common + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.Common") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Common/AlkamiManifest.xml new file mode 100644 index 0000000..7a8d0fc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.Common + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.Common/Private/ConfigurationValues.ps1 b/Modules/Alkami.PowerShell.Common/Private/ConfigurationValues.ps1 new file mode 100644 index 0000000..53719ba --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Private/ConfigurationValues.ps1 @@ -0,0 +1,68 @@ +## This PS1 Has Values Referenced By Multiple Functions/Scripts Which May Be Overridden By the User + +# This can be modified if needed, but probably shouldn't be +$global:basePath = Get-OrbPath -WarningAction SilentlyContinue -InformationAction SilentlyContinue + +# This can be modified if needed, but probably shouldn't be +$global:logsPath = Get-OrbLogsPath -WarningAction SilentlyContinue -InformationAction SilentlyContinue + +### These values will be instrumented from Secret Server if Install\-ORBAppServer is called with the necessary credentials +### In the case of a standalone or on-prem installation, complete the necessary fields +$global:appTierApplications = @( + @{ Name = "AuditService"; WebAppName = "AuditService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "AuditReportingService.svc"; VIPSuffix="61"; }, + @{ Name = "BankService"; WebAppName = "BankService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "BankService.svc"; VIPSuffix="50"; }, + @{ Name = "ContentService"; WebAppName = "ContentService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "ContentService.svc"; VIPSuffix="51"; }, + @{ Name = "CoreService"; WebAppName = "CoreService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "CoreService.svc"; VIPSuffix="52"; }, +# NOP this service because it's been deprecated since 2020.5 releases, and we are at 2022.3 as of this comment nearly everywhere +# @{ Name = "ExceptionService"; WebAppName = "ExceptionService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "ExceptionService.svc"; VIPSuffix="53"; }, + @{ Name = "MessageCenterService"; WebAppName = "MessageCenterService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "MessageCenterService.svc"; VIPSuffix="54"; }, + @{ Name = "NagConfigurationService"; WebAppName = "NagConfigurationService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "NagConfigurationService.svc"; VIPSuffix="55"; }, + @{ Name = "NotificationService"; WebAppName = "NotificationService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "NotificationService.svc"; VIPSuffix="62"; }, + @{ Name = "RP-STS"; WebAppName = "RP-STS"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "ClaimTransformation.svc"; VIPSuffix="60"; }, + @{ Name = "Scheduler"; WebAppName = "SchedulerService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "SchedulerService.svc"; VIPSuffix="56"; }, + @{ Name = "SecurityManagementService"; WebAppName = "SecurityManagementService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "SecurityManagement.svc"; VIPSuffix="57"; }, + @{ Name = "STSConfiguration"; WebAppName = "STSConfiguration"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "STSConfiguration.svc"; VIPSuffix="58"; }, + @{ Name = "SymConnectMultiplexer"; WebAppName = "SymConnectMultiplexer"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "Multiplexer.svc"; VIPSuffix="59"; } +) + +### These values will be instrumented from Secret Server if Install\-ORBAppServer is called with the necessary credentials +### In the case of a standalone or on-prem installation, complete the necessary fields +### This is intended for only installing legacy ORB Windows applications +$global:appTierServices = @( + @{ FolderName = 'Radium'; AssemblyInfo = 'Alkami.App.Radium.WindowsService'; Name = "Alkami Radium Scheduler Service"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Binary = $basePath + "\Radium\Alkami.App.Radium.WindowsService.exe"; }, + @{ FolderName = 'Nag'; AssemblyInfo = 'Alkami.App.Nag.Host.Service'; Name = "Alkami Nag Service"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Binary = $basePath + "\Nag\Alkami.App.Nag.Host.Service.exe"; } +) + +### These values will be instrumented from Secret Server if Install\-ORBAppServer is called with the requisite credentials and if the value is equal to "REPLACEME" +### In the case of a standalone or on-prem installation, provide the master database connection string +$global:masterConnectionString = "REPLACEME" + +### The broker should run as the local system account, this should not need to be modified +$global:webTierWindowsServices = @( +) + +### These values will be instrumented from Secret Server if Install-ORBWebServer is called with the necessary credentials +### In the case of a standalone or on-prem installation, complete the necessary fields +$global:webTierAppSettings = @( + @{ Name = "ReportServer"; Value = "REPLACEME"; }, + @{ Name = "ReportServerUrl"; Value = ("{0}/Pages/ReportViewer.aspx" -f ($webTierAppSettings | Where-Object {$_.Name -eq "ReportServer"}).Value); }, + @{ Name = "ReportServerPath"; Value = ("/{0}" -f [Environment]::GetEnvironmentVariable("POD", "Machine")); }, + @{ Name = "ReportServerUserName"; Value = "REPLACEME"; }, + @{ Name = "ReportServerPassword"; Value = "REPLACEME"; }, + @{ Name = "ReportUserName"; Value = "REPLACEME"; }, + @{ Name = "ReportPassword"; Value = "REPLACEME"; } +) + +### Redis Server Connection +### You can instrument this with the values Dev, QA, Staging, Prod, or AWS to have the correct value set automatically +### If you are not using one of these environments, you can put the full string here to have it set explicitly +### To keep the existing value, do not change the below +$global:redisEndpoint = "REPLACEME" + +### Keys will be automatically generated when not found using the POD designation as a seed +### Or you can set them manually here. If changed from the defaults below the keys will not be generated. +$global:machineKeyValidationKey = "C153B7375BE81D1B1F01D9AB2F8ED31E4CECD7A7EA226DF91EF737E0C3E5A081D07B1883BA80B866EF666B837D839A0739E22506F044148CF8F35854A3CD0472" +$global:machineKeyDecryptionKey = "2701BE9B42AAC5769232FFFE894C086B6838C613481B2694C975BCAC02807BD6" + +### This probably never needs to change. If you do change it, you need to manually set the Validation and Decryption keys to the appropriate lengths +$global:decryptionMethod = "AES" \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.Common/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..99f9f45 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Private/VariableDeclarations.ps1 @@ -0,0 +1,26 @@ +# A UTF-8 Encoding with no Byte-Order Mark. Not a built in encoding but good for XML. +$Global:UTFNoBOM = New-Object System.Text.UTF8Encoding($false) + +# A Global Lookup of .NET Registry Values to Friendly Versions +$Global:DotNetVersionTranslation = @( + + @{Key="378389"; FriendlyVersion="4.5";}, + @{Key="378675"; FriendlyVersion="4.5.1";}, + @{Key="378758"; FriendlyVersion="4.5.1";}, + @{Key="379893"; FriendlyVersion="4.5.2";}, + @{Key="393295"; FriendlyVersion="4.6";}, + @{Key="393297"; FriendlyVersion="4.6";}, + @{Key="394254"; FriendlyVersion="4.6.1";}, + @{Key="394271"; FriendlyVersion="4.6.1";}, + @{Key="394802"; FriendlyVersion="4.6.2";}, + @{Key="394806"; FriendlyVersion="4.6.2";}, + @{Key="460798"; FriendlyVersion="4.7";}, + @{Key="460805"; FriendlyVersion="4.7";}, + @{Key="461308"; FriendlyVersion="4.7.1";}, + @{Key="461310"; FriendlyVersion="4.7.1";}, + @{Key="461808"; FriendlyVersion="4.7.2";}, + @{Key="461814"; FriendlyVersion="4.7.2";}, + @{Key="528040"; FriendlyVersion="4.8.0";}, + @{Key="528049"; FriendlyVersion="4.8.0";}, + @{Key="528449"; FriendlyVersion="4.8.0";} +) diff --git a/Modules/Alkami.PowerShell.Common/Public/Add-DirectoryToPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Add-DirectoryToPath.ps1 new file mode 100644 index 0000000..e70a5dd --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Add-DirectoryToPath.ps1 @@ -0,0 +1,33 @@ +function Add-DirectoryToPath { +<# +.SYNOPSIS + Adds a Path to the System Path Ennvironment Variable +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$directory + ) + + $logLead = (Get-LogLeadName); + + if (!(Test-Path $directory)) { + Write-Warning ("$logLead : Path {0} does not exist, it will not be added to the PATH variable" -f $directory) + return + } + + $currentPathValue = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine) + Write-Verbose ("$logLead : Current Path Value: {0}" -f $currentPathValue) + + if ($currentPathValue -like ("*{0}*" -f $directory)) { + Write-Warning ("$logLead : Directory {0} already exists in the PATH variable" -f $directory) + return + } + + $newPathValue = $currentPathValue.TrimEnd(";") + ";" + $directory + Write-Output ("$logLead : Updating Path Value to {0}" -f $newPathValue) + + [Environment]::SetEnvironmentVariable("Path", $newPathValue, [System.EnvironmentVariableTarget]::Machine) | Out-Null +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.ps1 b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.ps1 new file mode 100644 index 0000000..b8a95b8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.ps1 @@ -0,0 +1,64 @@ +function Add-HostsFileContent { +<# +.SYNOPSIS + Adds Strings to the Hosts File +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string[]]$contentToAdd, + + [Parameter(Mandatory = $false)] + [string]$hostsPath = "$env:windir\System32\Drivers\etc\hosts", + + [Parameter(Mandatory = $false)] + [Alias("Force")] + [switch]$forceWrite + ) + + $logLead = (Get-LogLeadName) + $hostsContent = Get-HostsFileContent $hostsPath + $builder = New-Object System.Text.StringBuilder(($hostsContent -join [Environment]::NewLine)) + $builder.AppendLine() | Out-Null + + $hostsFileIsDirty = $false + [Regex]$uncommentedIPRegex = "^[^\#]?(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b" + + foreach ($hostsEntry in $contentToAdd) { + # Remove comments from the line. + $commentSearch = $hostsEntry.IndexOf("#"); + if($commentSearch -ge 0) { + $hostsEntry = $hostsEntry.Substring(0, $commentSearch); + } + if ($hostsEntry -match $uncommentedIPRegex) { + $ipFromContent = ($MATCHES.Values | Select-Object -First 1) + + if ($hostsContent -match $ipFromContent -and !($forceWrite.IsPresent)) { + Write-Warning ("$logLead : A hosts entry already exists for IP address: {0}" -f $ipFromContent) + continue + } + } + else { + Write-Warning ("$logLead : Could not find a valid IP address in content: {0}" -f $hostsEntry) + continue + } + + Write-Output ("$logLead : Adding Hosts Entry : {0}" -f $hostsEntry.ToString()) + $hostsFileIsDirty = $true + $builder.AppendLine($hostsEntry.ToString()) | Out-Null + } + + ## Ensure that a path exists. If the value is $null or empty, then we want "do we have a host path" to be $false, so invert the "is it null/empty" check value + ## TODO: cbrand ~ Do we want to add a Test-Path here instead? + $hasHostsPath = !([string]::IsNullOrEmpty($hostsPath)) + + if ($hostsFileIsDirty -and $hasHostsPath) { + Write-Output ("$logLead : Saving Modified Hosts File to {0}" -f $hostsPath) + + $finalOutput = $builder.ToString().TrimEnd(); + [System.IO.File]::WriteAllLines($hostsPath, $finalOutput); + } + else { + Write-Output ("$logLead : No changes made to hosts file") + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.tests.ps1 new file mode 100644 index 0000000..ddc209d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileContent.tests.ps1 @@ -0,0 +1,63 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Add-HostsFileContent + +Describe "Add-HostsFileContent" { + Mock -CommandName Write-Warning {} -ModuleName $moduleForMock + + Context "When an existing IP address is supplied" { + + Mock -CommandName Get-HostsFileContent { + return @("127.0.0.1 foo.bar", "192.168.1.1 bar.foo") + } -ModuleName $moduleForMock + + It "Writes a Warning" { + Add-HostsFileContent "127.0.0.1 hello.world" + Assert-MockCalled -CommandName "Write-Warning" -ParameterFilter {$Message -match "hosts entry already exists"} -ModuleName $ModuleToMock + } + + It "Does Not Save the Hosts File" { + + Add-HostsFileContent "127.0.0.1 hello.world" + + $hostsFile = Get-HostsFileContent + $hostsFile -match "hello.world" | Should -BeNullOrEmpty + } + } + + Context "When Valid Parameters are Supplied" { + + $tempPath = [System.IO.Path]::GetTempFileName() + + Mock -CommandName Get-HostsFileContent { + + return @("127.0.0.1 foo.bar", "192.168.1.1 bar.foo") + } -ModuleName $moduleForMock + + It "Adds a Single Host Entry" { + + Add-HostsFileContent "10.10.10.10 Hello.World" $tempPath + $content = Get-Content $tempPath + + $content -match "Hello.World" | Should -BeTrue + } + + It "Adds Multiple Host Entries" { + + $mockContent = @("10.10.10.10 Hello.World", "9.9.9.9 No.Way.Jose") + + Add-HostsFileContent $mockContent $tempPath + $content = Get-Content $tempPath + + $content -match "Hello.World" -and $content -match "No.Way.Jose" | Should -BeTrue + } + } +} + +#endregion Add-HostsFileContent \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.ps1 b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.ps1 new file mode 100644 index 0000000..d7d66d4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.ps1 @@ -0,0 +1,114 @@ +function Add-HostsFileEntry { + <# +.SYNOPSIS + Add Single entry to the Hosts File +.PARAMETER Ip + IP address for entry +.PARAMETER Hostname + Hostname for entry +.PARAMETER Comment + Optional comment for entry +.PARAMETER HostsPath + optional alternate path to hosts file +.Notes + This function does not support multiple host entrys per ip address on a single line + This function will comment out an entry with the same hostname as the new entry, in case you want to rollback +.EXAMPLE + Add-HostsFileEntry -Ip "10.10.10.10" -Hostname "hello.world" +.EXAMPLE + Add-HostsFileEntry -Ip "10.10.10.10" -Hostname "hello.world" -HostsPath "C:\Temp\bork.txt" +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [IpAddress]$Ip, + + [Parameter(Mandatory = $true)] + [string]$Hostname, + + [Parameter(Mandatory = $false)] + [string]$Comment = $null, + + [Parameter(Mandatory = $false)] + [string]$HostsPath = "$env:windir\System32\Drivers\etc\hosts" + ) + + $logLead = Get-LogLeadName + + if (($Hostname.split(" ")).Count -gt 1) { + Write-Error "$logLead : You cannot supply more than one hostname" + return + } + + try { + $hostsContent = Get-HostsFileContent -hostsPath $HostsPath + } catch [System.IO.IOException], [System.IO.FileNotFoundException] { + Write-Warning "$HostsPath is locked" + Write-Warning "$loglead : Reinitiating after 5 seconds..." + Start-Sleep -Seconds 5 + $hostsContent = Get-HostsFileContent -hostsPath $HostsPath + } catch { + Write-Warning "Error encountered when attempting to read file." + Write-Warning $_ + throw + } + + $hostsEntriesRebuilt = @() + $hostsFileIsDirty = $false + $foundMatchingLine = $false + [Regex]$matchAndSplitPattern = '^(?[0-9.]+)\s+(?[\w.-]+)' + + $hostsEntry = "$Ip $Hostname" + if ( [string]::IsNullOrWhitespace($Comment) -eq $false) { + $hostsEntry = "$hostsEntry # $Comment" + } + + foreach ($hostLine in $hostsContent) { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '', Justification = 'We want to ensure the variable is not holding onto stale information')] + $MATCHES = $null + if ($hostLine -match $matchAndSplitPattern) { + # MATCHES is an automatic variable, use it carefully https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-5.1#matches + $matchedHostname = $MATCHES["HostName"] + + if ($Hostname -eq $matchedHostname) { + Write-Warning "$logLead : A hosts entry already exists for entry: $matchedHostname, it will be commented out" + $hostsEntriesRebuilt += "#" + $hostLine + $hostsEntriesRebuilt += $hostsEntry + $hostsFileIsDirty = $true + $foundMatchingLine = $true + Continue + } + } + $hostsEntriesRebuilt += $hostLine + } + if ($foundMatchingLine -eq $false) { + Write-Host "$logLead : Adding Hosts Entry : $($hostsEntry.ToString())" + $hostsFileIsDirty = $true + $hostsEntriesRebuilt += $hostsEntry + } + if ($hostsFileIsDirty -eq $true) { + if (Test-IsCollectionNullOrEmpty -Collection $hostsEntriesRebuilt) { + Write-Warning "Rebuilt file content is empty, exiting..." + throw + } else { + try { + Write-Host "$logLead : Saving Modified Hosts File to $HostsPath" + Set-Content -Path $HostsPath -Value $hostsEntriesRebuilt -Force + Write-Host "$logLead : Saved Modified Hosts File to $HostsPath" + } catch [System.IO.IOException], [System.IO.FileNotFoundException] { + Write-Warning "$loglead : $HostsPath is locked" + Write-Warning "$loglead : Reinitiating after 5 seconds..." + Start-Sleep -Seconds 5 + Write-Host "$logLead : Attempt #2 : Saving Modified Hosts File to $HostsPath" + Set-Content -Path $HostsPath -Value $hostsEntriesRebuilt -Force + Write-Host "$logLead : Attempt #2 : Saved Modified Hosts File to $HostsPath" + } catch { + Write-Warning "Error encountered when attempting to write file." + Write-Warning $_ + throw + } + } + } else { + Write-Host"$logLead : No changes made to hosts file" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.tests.ps1 new file mode 100644 index 0000000..161feda --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Add-HostsFileEntry.tests.ps1 @@ -0,0 +1,76 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Add-HostsFileContent" { + Mock -CommandName Write-Warning {} -ModuleName $moduleForMock + Mock -CommandName Write-Host {} -ModuleName $moduleForMock + Mock -CommandName Write-Error {} -ModuleName $moduleForMock + Mock -CommandName Test-IsCollectionNullOrEmpty { $false } -ModuleName $moduleForMock + $tempPath = Join-Path $TestDrive "hoststFile.txt" + Set-Content -Path $tempPath @" +# This is a sample HOSTS file used by Microsoft TCP/IP for Windows. +10.10.10.10 bork.world +25.63.10 x.acme.com # x client host +127.0.0.1 already.exists +7.7.7.7 tobeforce.write +11.11.11.11 comment.dupe + +127.0.0.1 jw smartsite.jw +127.0.0.1 dev.filepresso.com dev.filepresso.com.pl dev.filepresso.pl dev.filefly.pl +#### 89.31.66.248 sp.biatelbit.pl +192.168.72.51 subversion.biatel.com.pl +# 127.0.0.1 bip.smartsite.bit-sa.pl + # 127.0.0.1 bip.augustow.wrotapodlasia.pl +127.0.0.1 rpowp.smartsite.bit-sa.pl +127.0.0.1 cms.smartsite.bit-sa.pl +127.0.0.1 cms-test.smartsite.bit-sa.pl +############################################################ +#10.200.14.70 ST-sapp1 +#10.200.14.70 tv.smartsite.bit-sa.pl wp.smartsite.bit-sa.pl cms.smartsite.bit-sa.pl test.smartsite.bit-sa.pl bip.smartsite.bit-sa.pl si.smartsite.bit-sa.pl sspw.wrotapodlasia.pl +#10.200.14.70 radny.smartsite.bit-sa.pl stats.smartsite.bit-sa.pl test9.smartsite.bit-sa.pl +"@ + + Context "Negative cases" { + It "Overwrites hostname" { + Add-HostsFileEntry -Ip "127.0.0.1" -Hostname "not.exist" -HostsPath $tempPath + $hostsFile = Get-HostsFileContent -HostsPath $tempPath + $hostsFile -match "127.0.0.1 not.exist" | Should -BeTrue + } + It "Does not add multihost" { + Add-HostsFileEntry -Ip "9.9.9.9" -Hostname "bork.world blarg.host" -Comment "foo" -HostsPath $tempPath + $content = Get-Content $tempPath + $content -match "9.9.9.9 bork.world blarg.host # foo" | Should -BeNullOrEmpty + } + } + + Context "Happy path" { + It "Comments out the dupe entry" { + Add-HostsFileEntry -Ip "11.11.11.11" -Hostname "comment.dupe" -HostsPath $tempPath + Add-HostsFileEntry -Ip "8.8.8.8" -Hostname "comment.dupe" -HostsPath $tempPath + $content = Get-Content $tempPath + $content -match "#11.11.11.11 comment.dupe" | Should -BeTrue + } + It "Adds line" { + Add-HostsFileEntry -Ip "9.9.9.9" -Hostname "bork.world" -HostsPath $tempPath + $content = Get-Content $tempPath + $content -match "9.9.9.9 bork.World" | Should -BeTrue + } + It "Adds line with comment" { + Add-HostsFileEntry -Ip "4.4.4.4" -Hostname "comment.line" -Comment "foo" -HostsPath $tempPath + $content = Get-Content $tempPath + $content -match "4.4.4.4 comment.line # foo" | Should -BeTrue + } + } + + Context "Bad override" { + It "Throws if array is null" { + Mock -CommandName Test-IsCollectionNullOrEmpty { $true } + { Add-HostsFileEntry -Ip "11.11.11.12" -Hostname "comment.duplicate" -HostsPath $tempPath } | Should Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiLogs.ps1 b/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiLogs.ps1 new file mode 100644 index 0000000..e0d7c87 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiLogs.ps1 @@ -0,0 +1,28 @@ +function Backup-AlkamiLogs { +<# +.SYNOPSIS + Archives logs based off a cutoff date and then cleans the .NET temporary files +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("LogPath")] + [string]$logDirectory, + + [Parameter(Mandatory=$false)] + [Alias("CutoffThreshold")] + [int]$cutoffDays = 1, + + [Parameter(Mandatory=$false)] + [Alias("SkipActive")] + [switch]$skipActiveLogs + ) + + if ([string]::IsNullOrEmpty($logDirectory)) { + $logDirectory = (Get-OrbLogsPath) + } + + Backup-ORBLogFiles -skipActiveLogs:$skipActiveLogs.IsPresent; + + Remove-OldArchivedLogFiles -ArchivePath (Join-Path $logDirectory "Archive") -CutoffThreshold $cutoffDays; +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiOrb.ps1 b/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiOrb.ps1 new file mode 100644 index 0000000..2a5f86b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Backup-AlkamiOrb.ps1 @@ -0,0 +1,47 @@ +function Backup-AlkamiOrb { +<# +.SYNOPSIS + Compress C:\Orb to C:\tools\Backup folder +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$orbPath, + + [Parameter(Mandatory=$false)] + [string]$backupPath = "c:\Tools\Backups\", + + [Parameter(Mandatory=$false)] + [int]$cutoffDays = 15 + ) + + $logLead = (Get-LogLeadName); + $datetime = Get-Date -Format "MM_dd_yyyy_HHmmss" + $absoluteCutoff = [System.Math]::Abs($cutoffDays) + $cutoffDate = (Get-Date).AddDays($absoluteCutoff * -1) + + if([string]::IsNullOrEmpty($orbPath)) { + $orbPath = (Get-OrbPath) + } + + if (!(Test-Path $backupPath)) { + (New-Item -path $backupPath -ItemType Directory) | Out-Null + } + + if (Test-Path $orbPath) { + try { + Write-Output ("$logLead : Backup Files from {0} to {1}" -f $orbPath,$backupPath ) + $zipname = "$($backupPath)Orb_$($datetime).zip" + 7z.exe a -tzip -y $zipname $orbPath -snl + } catch { + Throw ("$logLead : Archive Failed {0}" -f $zipname ) + } finally { + Write-Output ("$logLead : Archive Completed {0}" -f $zipname ) + } + } else { + Throw ("$logLead : Path not found {0}" -f $orbPath ) + } + + Write-Output ("$logLead : Deleting Old Backups") + Get-childitem $backupPath | Where-Object { ! $_.PsIsContainer -and $_ -match '^Orb_.*.zip'}| Where-Object { $_.CreationTimeUtc -lt [System.TimeZoneInfo]::ConvertTimeToUtc($cutOffDate) } | Select-Object -ExpandProperty FullName | Remove-item +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Backup-LogFiles.ps1 b/Modules/Alkami.PowerShell.Common/Public/Backup-LogFiles.ps1 new file mode 100644 index 0000000..f16258c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Backup-LogFiles.ps1 @@ -0,0 +1,114 @@ +function Backup-LogFiles { +<# +.SYNOPSIS + Saves old logs into an Archive subfolder. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("LogPath")] + [string]$logDirectory, + + [Parameter(Mandatory = $false)] + [Alias("CutoffDays")] + [int]$archiveOlderThanDays = 0, + + [Parameter(Mandatory = $false)] + [string]$filter = "*.log*" + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrEmpty($logDirectory)) { + $logDirectory = (Get-OrbLogsPath) + } + + if (!(Test-Path $logDirectory)) { + Write-Warning "$logLead : Directory `"$logDirectory`" not found. Skipping."; + return; + } + + $archivePath = Join-Path $logDirectory "Archive"; + + Write-Host "$logLead : Backing up / Archiving logs in `"$logdirectory`""; + + if (!(Test-Path $archivePath)) { + New-Item $archivePath -ItemType Directory | Out-Null; + } + + # Determine the cutoff date to archive files. + $absoluteCutoff = [System.Math]::Abs($archiveOlderThanDays); + $cutoffDate = (Get-Date).AddDays($absoluteCutoff * -1); + $cutoffUtc = [System.TimeZoneInfo]::ConvertTimeToUtc($cutoffDate); + + # Remove any potential trailing wildcarded directories + while ($logDirectory.Substring($logDirectory.get_length() - 2) -eq "\*" ) { + Write-Host "Found a wildcard. Trimming..." + $logDirectory = $logDirectory.Substring(0, $logDirectory.get_length() - 2) + } + + # Tack on a single \* so that Get-ChildItem plays nicely with both the exclude and filter parameters + + $logDirectory = $logDirectory + "\*" + + # Figure out which files need to be archived. + $logFiles = Get-ChildItem $logDirectory -Exclude *.zip, *.7z -Filter $filter -File | Where-Object { $_.LastWriteTimeUtc -lt $cutoffUtc } + + # Archive each file. + $szPath = (Get-7ZipPath) + foreach ($file in $logFiles) { + Write-Verbose "Archiving $($file.FullName)"; + + $archiveZip = $file.LastWriteTimeUtc.ToString("yyyy-MM-dd"); + $archiveZip = "$archiveZip.zip" + $archiveZip = Join-Path $archivePath $archiveZip + $archiveExists = Test-Path $archiveZip; + + # Figure out if the file is already in the zip. + # We have to rename a file, and then add it, to prevent the original file in the zip from being overwritten. + # This is particularly an issue if we do multiple deploys inside a day, which would overwrite the active log. + $fileToArchive = $file.FullName; + $tempPath = $null; + try { + if ($archiveExists) { + # Search files inside the archive. + $archiveFiles = (& $szPath l -r $archiveZip) | Where-Object { $_ -like "*$file*" -or $_ -like "*$($file.BaseName)_*$($file.Extension)" } + $fileCount = $archiveFiles.count; + + if ($fileCount -gt 0) { + Write-Verbose "File name $($file.Name) already exists in the archive `"$archiveZip`" Creating a temporary log file copy & adding it to the archive."; + + # Turns "C:/some/base/path/logname.log" into "C:/some/base/path/logname_2.log" + $tempFileName = "$($file.BaseName)_$fileCount$($file.Extension)" + $tempPath = Join-Path $file.Directory $tempFileName; + + Copy-Item -Path $file.FullName -Destination $tempPath + $fileToArchive = $tempPath; + } + } + + try { + & $szPath a -tzip -sdel -y $archiveZip $fileToArchive; + } catch { + Write-Warning $_.Exception; + } + + # Clean up the original file if there was a pre-existing file in the archive. + # 7-Zip removes a file when it adds it to an archive. + if ($tempPath -and (Test-Path $file.FullName)) { + Write-Verbose "Removing log $($file.FullName) because it was a duplicate filename in the `"$archiveZip`" archive." + Remove-Item -Path $file.FullName -Force; + } + } catch { + Write-Warning $_.Exception; + } finally { + # Remove temporary log files if zipping failed out for any reason. + if ($tempPath -and (Test-Path $tempPath)) { + Write-Verbose "Removing temporary log file copy `"$tempPath`""; + Remove-Item -Path $tempPath -Force; + } + } + } + + Write-Host "$logLead : Backup complete."; +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.ps1 b/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.ps1 new file mode 100644 index 0000000..b520ca9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.ps1 @@ -0,0 +1,95 @@ +function Backup-ORBLogFiles { + <# +.SYNOPSIS + Saves old ORB Logs in an Archive subfolder +.PARAMETER logDirectory + Directory of orb logs +.PARAMETER skipActiveLogs + Should we skip the logs of running services? +.EXAMPLE + Backup-OrbLogFiles +.EXAMPLE + Backup-OrbLogFiles -logDirectory Get-OrbLogsPath -skipActiveLogs +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("LogPath")] + [string]$logDirectory, + + [Parameter(Mandatory = $false)] + [Alias("SkipActive")] + [switch]$skipActiveLogs + ) + + $logLead = Get-LogLeadName + + if ([string]::IsNullOrEmpty($logDirectory)) { + $logDirectory = Get-OrbLogsPath + } + + $datetime = Get-Date -Format "MM_dd_yy_HHmmss" + $archivePath = Join-Path $logDirectory "Archive" + $7zipPath = Get-7ZipPath + if (!(Test-Path $archivePath)) { + New-Item -Path $archivePath -ItemType Directory | Out-Null + } + + $filter = if ($skipActiveLogs) { "*.log.*" } else { "*.log*" } + + # Remove any potential trailing wildcarded directories + while ($logDirectory.Substring($logDirectory.get_length() - 2) -eq "\*" ) { + Write-Host "$logLead : Found a wildcard. Trimming..." + $logDirectory = $logDirectory.Substring(0, $logDirectory.get_length() - 2) + } + + # Tack on a single \* so that Get-ChildItem plays nicely with both the Include and Exclude parameters + $logDirectory = $logDirectory + "\*" + + $logFiles = @() + if (Test-Path -Path $logDirectory -Include $filter -Exclude "*.lnk") { + $logFiles += [array](Get-ChildItem -Path $logDirectory -Include $filter -Exclude "*.lnk" -File) -notmatch "\d{12}" + } + + # Slog files get saved with the date stamp in the filename + # For example: Alkami.Services.BillPayOrchestration-20210901.slog + # Super annoying + # Slog files also have file locks unless they've been written to, even if the date has rolled + # This should handle most cases + if ($skipActiveLogs) { + $slogDateFormat = (Get-Date).ToString("yyyyMMdd") + if (Test-Path -Path $logDirectory -Include "*.slog*" -Exclude "*$slogDateFormat.slog*") { + $logFiles += [array](Get-ChildItem -Path $logDirectory -Include "*.slog*" -Exclude "*$slogDateFormat.slog*" -File) -notmatch "\.(gz|7z|zip)" + } + + } else { + if (Test-Path -Path $logDirectory -Include "*.slog*" ) { + $logFiles += [array](Get-ChildItem -Path $logDirectory -Include "*.slog*" -File) -notmatch "\.(gz|7z|zip)" + } + } + + $uniqueLogFiles = ($logFiles.Basename -replace "(.log|-\d{8})" , "") | Sort-Object | Get-Unique + + Write-Host "$logLead : Backing up $archivePath files to $filter" + + foreach ($uniqueLogFile in $uniqueLogFiles) { + $logs = $logFiles | Where-Object { ($_.BaseName -replace "(.log|-\d{8})" , "") -eq $uniqueLogFile } + + if ($logs) { + Write-Host "$logLead : Compressing Files" + Write-Host $logs.FullName + $zipname = "$($archivePath)\$($uniqueLogFile)\$($env:COMPUTERNAME)_$($datetime).zip" + Write-Host "$logLead : Archive Files | $($zipname)" + try { + if ($null -ne $logs.FullName) { + Write-Verbose "$logLead : Invoking 7zip with: a -tzip -sdel -y $zipname $($logs.FullName)" + & $7zipPath a -tzip -sdel -y $zipname $logs.FullName + } + } catch { + Write-Warning $_.Exception + } + } + } + Write-Host ("$logLead : Backup Complete") +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.tests.ps1 new file mode 100644 index 0000000..e2747c1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Backup-ORBLogFiles.tests.ps1 @@ -0,0 +1,42 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Backup-ORBLogFiles" { + $logDirectory = "TestDrive:\orblogs" + Mock -CommandName Write-Warning -ModuleName $moduleForMock {} + Mock -CommandName Get-OrbLogsPath -ModuleName $moduleForMock { return "TestDrive:\orblogs\" } + function Invoke-CallZippy {} + Mock -CommandName Get-7ZipPath -ModuleName $moduleForMock { return Invoke-CallZippy } + Mock -CommandName invoke-callzippy -ModuleName $moduleForMock {} + Mock -CommandName Write-Host -ModuleName $moduleForMock {} + Mock -CommandName Write-Verbose -ModuleName $moduleForMock {} + Mock -CommandName Test-Path -ModuleName $moduleForMock { return $true } + New-item -Path $logDirectory -ItemType Directory + + + Context "Ensure correct log folder is archived" { + New-item -Path "$logDirectory\alkami.ms.log" -ItemType File + $logfile = Get-item "$logDirectory\alkami.ms.log" + + It "Writes host of logfile to compress" { + Backup-ORBLogFiles + Assert-MockCalled -CommandName "Write-Host" -ParameterFilter { $Object -eq $logfile.FullName } -ModuleName $moduleForMock + } + It "Creates Archive folder in correct place" { + Backup-ORBLogFiles + Assert-MockCalled -ModuleName $moduleForMock -CommandName "Test-Path" -ParameterFilter { $Path -eq "$logDirectory\Archive" } + } + Remove-Item -Path $logfile + } + Context "Does not archive if files don't exist" { + It "Does not invoke call zippy" { + Backup-ORBLogFiles + Assert-MockCalled -CommandName "Write-Host" -ParameterFilter { $Object -eq "Invoking 7zip" } -ModuleName $moduleForMock -Times 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.ps1 b/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.ps1 new file mode 100644 index 0000000..2aa11ed --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.ps1 @@ -0,0 +1,71 @@ +function Close-SMBApplicationLocks { +<# +.SYNOPSIS + Closes Active SMB Sessions for Default or User Supplied Paths + +.DESCRIPTION + This function is used to prevent interruption to deployments by closing any SMB locks + in application paths. Defaults to closing sessions in folders matching regex + "ORBLogs|ORB|CHOCOLATEY|WinTest" + +.PARAMETER Paths + [string[]] A string array of paths or path segments to match sessions against. + +.EXAMPLE + Close-SMBApplicationLocks + +[Close-SMBApplicationLocks] : Closing FileId 3588176742489 on SMB Session 2717036577625 for user CORP\cmcdonald in path C:\ORBLogs +[Close-SMBApplicationLocks] : Closing FileId 3588176764644 on SMB Session 2717036577625 for user CORP\cmcdonald in path C:\ORBLogs + +.EXAMPLE + Close-SMBApplicationLocks -Paths @("TEMP") + +[Close-SMBApplicationLocks] : Closing FileId 3588176742489 on SMB Session 2717103686265 for user CORP\dsage in path C:\Temp +[Close-SMBApplicationLocks] : Closing FileId 3588176742765 on SMB Session 2717103686265 for user CORP\dsage in path C:\temp\deploy +[Close-SMBApplicationLocks] : Closing FileId 3588176742324 on SMB Session 2717103686265 for user CORP\dsage in path C:\temp\deploy +[Close-SMBApplicationLocks] : Closing FileId 3588176746854 on SMB Session 2717103686265 for user CORP\dsage in path C:\Temp +[Close-SMBApplicationLocks] : Closing FileId 3588176787652 on SMB Session 2717103686265 for user CORP\dsage in path C:\Temp +[Close-SMBApplicationLocks] : Closing FileId 3588176712345 on SMB Session 2717103686265 for user CORP\dsage in path C:\temp\deploy +#> + [CmdletBinding()] + param( + [Alias("SharePaths")] + [Parameter(Mandatory=$false)] + [string[]]$Paths = @("ORBLogs","ORB","CHOCOLATEY","WinTest") + ) + + $logLead = Get-LogLeadName + + $pathsExpression = $Paths -join "|" + Write-Host ("$logLead : Looking for SMB Sessions Matching Path: {0}" -f $pathsExpression) + + $smbSessions = @(Get-SmbOpenFile | Where-Object {$_.Path -match $pathsExpression}) + + if ((Test-IsCollectionNullOrEmpty -Collection $smbSessions)) { + Write-Host "$logLead : No Matching SMB Sessions Found" + return + } + + Write-Host "$logLead : Found $($smbSessions.Count) Matching SMB Sessions" + + $uniqueFileIds = ($smbSessions).FileId | Sort-Object -Unique + + foreach ($fileId in $uniqueFileIds) { + $session = @($smbSessions | Where-Object { $_.FileId -eq $fileId })[0] + + $sessionId = $session.SessionId + $username = $session.ClientUserName + $path = $session.Path + + Write-Host "$logLead : Closing FileId $fileId on SMB Session $sessionId for user $username in path $path" + + try { + ## Yes this is FOUR ways to suppress output. Microsoft has proven remarkably resilient at showing an error here. + ## This is a CIM function that we can't force to behave like PowerShell so ... good times + (Close-SmbOpenFile -FileId $fileId -Force -ErrorAction Ignore *>&1) | Out-Null + } catch { + $errorMessage = $_.Exception.Message + Write-Warning "$logLead : An Error Occurred While Trying to Close Session $sessionId : $errorMessage" + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.tests.ps1 new file mode 100644 index 0000000..29959d5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Close-SMBApplicationLocks.tests.ps1 @@ -0,0 +1,90 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Close-SMBApplicationLocks" { + + #Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith {} + + + Context "Parameter Validation" { + + Mock -ModuleName $moduleForMock Close-SmbOpenFile {} + Mock -ModuleName $moduleForMock Get-SmbOpenFile { + $orbProc = New-Object -TypeName PSObject -Property @{ + Path = "C:\Orb" + SessionId = "1" + FileId = 27 + ClientUserName = "Pester" + } + $customProc1 = New-Object -TypeName PSObject -Property @{ + Path = "C:\TestPath1" + SessionId = "2" + FileId = 28 + ClientUserName = "Pester" + } + $customProc2 = New-Object -TypeName PSObject -Property @{ + Path = "C:\TestPath2" + SessionId = "3" + FileId = 29 + ClientUserName = "Pester" + } + + return @($orbProc, $customProc1, $customProc2) + } + #Mock -ModuleName $moduleForMock Get-SmbOpenFile -ParameterFilter { $FileId } -MockWith { return $true } + + It "Uses a Default Path List if Not Provided" { + Close-SMBApplicationLocks -Verbose + Assert-MockCalled -ModuleName $moduleForMock Close-SmbOpenFile -Times 1 -Exactly -Scope It + } + + It "Uses Custom Paths if Provided" { + Close-SMBApplicationLocks @("TestPath1","TestPath2") -Verbose + Assert-MockCalled -ModuleName $moduleForMock Close-SmbOpenFile -Times 2 -Exactly -Scope It + } + } + + Context "When No Sessions are Found" { + Mock -ModuleName $moduleForMock Close-SmbOpenFile {} + Mock -ModuleName $moduleForMock Get-SmbOpenFile { return $null } + + It "Does Not Call Close-SMBSession" { + + Close-SMBApplicationLocks -Verbose + Assert-MockCalled -ModuleName $moduleForMock Close-SmbOpenFile -Times 0 -Exactly -Scope It + } + } + + Context "WinTest Parameter Validation" { + Mock -ModuleName $moduleForMock Close-SmbOpenFile {} + Mock -ModuleName $moduleForMock Get-SmbOpenFile { + $winTestProc = New-Object -TypeName PSObject -Property @{ + Path = "C:\Tools\Wintest\Current" + SessionId = "5" + FileId = 24 + ClientUserName = "Pester" + } + $extraProc1 = New-Object -TypeName PSObject -Property @{ + Path = "C:\ExtraPath1" + SessionId = "6" + FileId = 27 + ClientUserName = "Pester" + } + + return @($winTestProc, $extraProc1) + } + It "Default Path List Closes WinTest standard location" { + + Close-SMBApplicationLocks -Verbose + Assert-MockCalled -ModuleName $moduleForMock Close-SmbOpenFile -Times 1 -Exactly -Scope It + } + + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Compare-MetadataPart.ps1 b/Modules/Alkami.PowerShell.Common/Public/Compare-MetadataPart.ps1 new file mode 100644 index 0000000..5990822 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Compare-MetadataPart.ps1 @@ -0,0 +1,55 @@ +function Compare-MetadataPart { +<# +.SYNOPSIS + Compare two version metadata parts for equivalence using the same pattern as Compare-SemVer. + This function principally intended to be called from Compare-SemVer, prefer that over this. + +.PARAMETER Version1Part + [string] A fragment of a version for comparing/evaluating just the fragment of the metadata + +.PARAMETER Version2Part + [string] A fragment of a version for comparing/evaluating just the fragment of the metadata +#> + [CmdletBinding()] + [OutputType([int])] + param( + [string]$Version1Part, + [string]$Version2Part + ) + + if ((-not $Version1Part) -and (-not $Version2Part)) { + return 0 + } + + # For release part, 1.0.0 is newer/greater then 1.0.0-alpha. So return 1 here. + if ((-not $Version1Part) -and $Version2Part) { + return 1 + } + + if (($Version1Part) -and (-not $Version2Part)) { + return -1 + } + + $version1Num = 0 + $version2Num = 0 + + $v1IsNumeric = [System.Int32]::TryParse($Version1Part, [ref] $version1Num); + $v2IsNumeric = [System.Int32]::TryParse($Version2Part, [ref] $version2Num); + + $result = 0 + # if both are numeric compare them as numbers + if ($v1IsNumeric -and $v2IsNumeric) { + $result = $version1Num.CompareTo($version2Num); + } elseif ($v1IsNumeric -or $v2IsNumeric) { + # numeric numbers come before alpha chars + if ($v1IsNumeric) { + return -1 + } else { + return 1 + } + } else { + $result = [string]::Compare($Version1Part, $Version2Part) + } + + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Compare-ReleaseMetadata.ps1 b/Modules/Alkami.PowerShell.Common/Public/Compare-ReleaseMetadata.ps1 new file mode 100644 index 0000000..78f409f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Compare-ReleaseMetadata.ps1 @@ -0,0 +1,67 @@ +function Compare-ReleaseMetadata { +<# +.SYNOPSIS + Compare two version release metadata parts for equivalence using the same pattern as Compare-SemVer. + This function principally intended to be called from Compare-SemVer, prefer that over this. + +.PARAMETER Version1Metadata + [string] A portion of a version for comparing/evaluating just the portion of the metadata + +.PARAMETER Version2Metadata + [string] A portion of a version for comparing/evaluating just the portion of the metadata +#> + [CmdletBinding()] + [OutputType([int])] + + param( + [string]$Version1Metadata, + [string]$Version2Metadata + ) + + if((-not $Version1Metadata) -and (-not $Version2Metadata)) + { + return 0 + } + + # For release part, 1.0.0 is newer/greater then 1.0.0-alpha. So return 1 here. + if((-not $Version1Metadata) -and $Version2Metadata) + { + return 1 + } + + if(($Version1Metadata) -and (-not $Version2Metadata)) + { + return -1 + } + + $version1Parts=$Version1Metadata.Trim('-').Split('.') + $version2Parts=$Version2Metadata.Trim('-').Split('.') + + $length = [System.Math]::Min($version1Parts.Length, $version2Parts.Length) + + for ($i = 0; ($i -lt $length); $i++) + { + $result = Compare-MetadataPart -Version1Part $version1Parts[$i] -Version2Part $version2Parts[$i] + + if ($result -ne 0) + { + return $result + } + } + + # so far we found two versions are the same. If length is the same, we think two version are indeed the same + if($version1Parts.Length -eq $version1Parts.Length) + { + return 0 + } + + # 1.0.0-alpha < 1.0.0-alpha.1 + if($version1Parts.Length -lt $length) + { + return -1 + } + else + { + return 1 + } + } diff --git a/Modules/Alkami.PowerShell.Common/Public/Compare-SemVer.ps1 b/Modules/Alkami.PowerShell.Common/Public/Compare-SemVer.ps1 new file mode 100644 index 0000000..4c321e0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Compare-SemVer.ps1 @@ -0,0 +1,120 @@ +function Compare-SemVer { +<# +.SYNOPSIS + Used to compare two semantic version (semver) strings, following the same pattern as traditionally established by Microsoft for comparisons. + This will return a -1 for less-than, +1 for greater-than, and 0 for equivalent values. + +.DESCRIPTION + Compare two sematic verions and return a signed integer to denote the result. This follows the typical Microsoft convention for such cases. + Examples: + -1 if $Version1 < $Version2 + 0 if $Version1 = $Version2 + 1 if $Version1 > $Version2 + + A different way to read that is: + -1 if $Version1 TRAILS $Version2 + 0 if $Version1 equals $Version2 + 1 if $Version1 LEADS $Version2 + + In this case, trails means "is behind" or "is older than" or "is lesser than" or "is inferior to" if considered on a number line. + Likewise, leads means "is ahead of" or "is newer than" or "is greater than" or "is superior to" if considered on a number line. + + When $Version1 TRAILS $Version2, by the definition, $Version2 LEADS $Version1. The reverse also holds. + + Q: What is a Semantic Version? What is a semver? + A: https://semver.org/ + + Summary + Given a version number MAJOR.MINOR.PATCH, increment the: + + MAJOR version when you make incompatible API changes, + MINOR version when you add functionality in a backwards compatible manner, and + PATCH version when you make backwards compatible bug fixes. + Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + + Based on the semver spec, if a pre(release) tag is provided for both versions, they are considered as part of this utility. + A 1.0.1-pre01 is not equivalent to a 1.0.1 by this function, and two pre-release versions with the same MAJOR.MINOR.PATCH are + parsed per the trailing value after any -pre denoting marker. + + Based on the semver spec, build metadata SHOULD be ignored when determining version precedence. + + If two variables are provided that are not parseable as versions, the function considers them to be equivalent. + If one variable is parseable as a version, but the other is not, the function considers the invalid value to be less-than the other. + +.PARAMETER Version1 + [string] This version is the one that will be compared against. + When this variable is not provided as a parseable version value, this value is considered "less-than". + When neither variable is provided as a parseable version vale, both values are considered equal. + +.PARAMETER Version2 + [string] This is the version that will be checked for less-than, equal, or greater-than the Version1 variable. + When this variable is not provided as a parseable version value, this value is considered "less-than". + When neither variable is provided as a parseable version vale, both values are considered equal. + +.EXAMPLE + Compare-SemVer -Version1 "1.0.1" -Version2 "1.0.2" + result: 1 + $Version2 is "newer" + +.EXAMPLE + Compare-SemVer -Version1 "1.0.2" -Version2 "1.0.2-pre1" + result: -1 + $Version1 is "newer" + +.EXAMPLE + Compare-SemVer -Version1 "1.0.2" -Version2 "1.0.2" + result: 0 + +.EXAMPLE + Compare-SemVer -Version1 "1.0.2-pre1" -Version2 "1.0.2" + result: -1 + $Version2 is "newer" + +.LINK + Reused by MIT license + https://github.com/jianyunt/ChocolateyGet/blob/d1b271b230307b8e35a3a8560633ddfdfe62885f/ChocolateyGet.psm1#L1091 +#> + [CmdletBinding()] + [OutputType([int])] + + param( + [Parameter(Mandatory)] + [string]$Version1, + [Parameter(Mandatory)] + [string]$Version2 + ) + + $versionObject1 = Get-VersionPSObject $Version1 + $versionObject2 = Get-VersionPSObject $Version2 + + if ((-not $versionObject1) -and (-not $versionObject2)) { + return 0 + } + + if ((-not $versionObject1) -and ($versionObject2)) { + return -1 + } + + if (($versionObject1) -and (-not $versionObject2)) { + return 1 + } + + $VersionResult = ([Version]$versionObject1.Version).CompareTo([Version]$versionObject2.Version) + if ($VersionResult -ne 0) { + return $VersionResult + } + + if ($versionObject1.Release -and (-not $versionObject2.Release)) { + return -1 + } + + if (-not $versionObject1.Release -and $versionObject2.Release) { + return 1 + } + + + $ReleaseResult = Compare-ReleaseMetadata -Version1Metadata $versionObject1.Release -Version2Metadata $versionObject2.Release + return $ReleaseResult + + # Based on http://semver.org/, Build metadata SHOULD be ignored when determining version precedence +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.ps1 new file mode 100644 index 0000000..6f7352a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.ps1 @@ -0,0 +1,36 @@ +function Compare-StringToLocalMachineIdentifiers { +<# +.SYNOPSIS + Compares a given string to see if it matches local machine identifiers (hostname, ip address) +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [string]$stringToCheck + ) + + $trimmedString = $stringToCheck.Trim() + + if (Test-IsStringIPAddress $trimmedString) { + $localIPV4Addresses = Get-NetIPAddress | Where-Object {$_.AddressFamily -eq "IPv4"} | Select-Object -ExpandProperty IPAddress + + if ($localIPV4Addresses -contains $trimmedString) { + return $true + } + + return $false + } + + $localHostMatches = @( + "localhost", + $env:COMPUTERNAME.ToLowerInvariant(), + (Get-FullyQualifiedServerName) + ) + + if ($localHostMatches -icontains $trimmedString) { + return $true + } + + return $false +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.tests.ps1 new file mode 100644 index 0000000..1af64b9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Compare-StringToLocalMachineIdentifiers.tests.ps1 @@ -0,0 +1,65 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Compare-StringToLocalMachineIdentifiers" { + + Context "Expected Matches" { + + $localIPAddresses = Get-NetIPAddress | Where-Object {$_.AddressFamily -eq "IPv4"} | Select-Object -ExpandProperty IPAddress + + $expectedMatches = @( + + "localhost", + $env:COMPUTERNAME, + "127.0.0.1", + (Get-FullyQualifiedServerName), + ($localIPAddresses | Select-Object -First 1) + ) + + foreach ($goodString in $expectedMatches) { + + It "Returns true for $goodString" { + + Compare-StringToLocalMachineIdentifiers $goodString | Should -BeTrue + } + } + } + + Context "Expected Failures" { + + $expectedFailures = @( + + "192.168.1.1", + "FakeHostName.google.com", + "@!@#!!!@(!()", + 13984984521, + -1 + ) + + foreach ($badString in $expectedFailures) { + + It "Returns false for $badString" { + + Compare-StringToLocalMachineIdentifiers $badString | Should -BeFalse + } + } + } + + Context "When a String is Piped To the Function" { + + It "Returns True if the String Matches" { + + "localhost" | Compare-StringToLocalMachineIdentifiers | Should -BeTrue + } + + It "Returns False if the String Does Not Match" { + + "9.9.9.9" | Compare-StringToLocalMachineIdentifiers | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-JsonToHashtable.ps1 b/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-JsonToHashtable.ps1 new file mode 100644 index 0000000..edb360c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-JsonToHashtable.ps1 @@ -0,0 +1,62 @@ +function ConvertFrom-JsonToHashtable { +<# +Copyright 2014 ASOS.com Limited +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +From: https://github.com/POSHChef/POSHChef/blob/master/functions/Exported/ConvertFrom-JSONtoHashtable.ps1 + +#> +<# +.SYNOPSIS + Helper function to take a JSON string and turn it into a hashtable + +.DESCRIPTION + The built in ConvertFrom-Json file produces as PSCustomObject that has case-insensitive keys. This means that + if the JSON string has different keys but of the same name, e.g. 'size' and 'Size' the comversion will fail. + Additionally to turn a PSCustomObject into a hashtable requires another function to perform the operation. + This function does all the work in step using the JavaScriptSerializer .NET class + +#> + [CmdletBinding()] + param( + + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [string] + $InputObject, + + [switch] + # Switch to denote that the returning object should be case sensitive + $CaseSensitive + ) + + # Perform a test to determine if the inputobject is null, if it is then return an empty hash table + if ([String]::IsNullOrEmpty($InputObject)) { + + $dict = @{} + + } else { + + # load the required dll + [void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions") + $deserializer = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer + $deserializer.MaxJsonLength = [int]::MaxValue + $dict = $deserializer.DeserializeObject($InputObject) + + # If the caseinsensitve is false then make the dictionary case insensitive + if ($CaseSensitive -eq $false) { + $dict = New-Object "System.Collections.Generic.Dictionary[System.String, System.Object]"($dict, [StringComparer]::OrdinalIgnoreCase) + } + + } + + return $dict +} diff --git a/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-Xml.ps1 b/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-Xml.ps1 new file mode 100644 index 0000000..0dde87a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/ConvertFrom-Xml.ps1 @@ -0,0 +1,64 @@ +function ConvertFrom-Xml { +<# +.SYNOPSIS + Used to convert an object from Xml to a hashtable (think `$xml | ConvertFrom-Xml | ConvertTo-Json`) + +.PARAMETER Node + Some XML node to be passed in +#> + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Xml.XmlNode]$Node + ) + + if ($node.DocumentElement) { + $Node = $Node.DocumentElement + } + + $name = $Node.Name + $orderedHashtable = [Ordered]@{ $name = [Ordered]@{} } + + foreach($attribute in $Node.Attributes) { + $attributeName = $attribute.Name + $orderedHashtable.$name.$attributeName = $attribute.FirstChild.InnerText + } + + foreach($child in $Node.ChildNodes) { + $childName = $child.Name + $value = $null + if ($child -is [System.Xml.XmlComment]) { + $childName = "xmlComments" + $value = $child.OuterXml + } elseif ($child -is [System.Xml.XmlText]) { + $value = $child.InnerText + $childName = $name + } else { + $value = (ConvertFrom-Xml $child).$childName + # this hackery lets an object like text become elem: { attr = x; elem = val; } but val becomes elem: val + # simple hack is best hack + # Thanks I hate it too + if (($null -ne $value.$childName) -and ($value.Keys.Count -eq 1)) { + $value = $value.$childName + } else { + $value = [PSCustomObject]$value + } + } + + if ($null -eq $value) { + continue + } + + if ($null -eq $orderedHashtable.$name.$childName) { + $orderedHashtable.$name.$childName = $value + } else { + # the key already exists, so we need to be using an array for the values + if (!($orderedHashtable.$name.$childName -is [Array])) { + $current = @($orderedHashtable.$name.$childName) + $orderedHashtable.$name.$childName = $current + } + $orderedHashtable.$name.$childName += $value + } + } + + return [PSCustomObject]$orderedHashtable +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.ps1 b/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.ps1 new file mode 100644 index 0000000..3fbc4b7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.ps1 @@ -0,0 +1,30 @@ +function ConvertTo-SafeTeamCityMessage { +<# +.SYNOPSIS + Converts strings to sanitized strings that are safe to use for TeamCity Service Messages. +.EXAMPLE + ConvertTo-SafeTeamCityMessage "I have a | pipe" + will return "I have a || pipe" +.INPUTS + Input: String + also can be null input +.OUTPUTS + Output: String +#> + param( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $InputText + ) + + $sanitizedText = $InputText + $charactersToEscape = @( "|", "'", "’", "[", "]") + foreach ($char in $charactersToEscape) { + $sanitizedText = $sanitizedText.Replace($char, "|$char") + } + + $sanitizedText = $sanitizedText.Replace("`r", "|r") + $sanitizedText = $sanitizedText.Replace("`n", "|n") + + return ($sanitizedText) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.tests.ps1 new file mode 100644 index 0000000..ae04d96 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/ConvertTo-SafeTeamCityMessage.tests.ps1 @@ -0,0 +1,93 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "ConvertTo-SafeTeamCityMessage" { + + Context "When Provided With Non-Newline Characters" { + It "Should Properly Sanitize |" { + $testString = "I have a | pipe" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "\|\|" + } + + It "Should Properly Sanitize '" { + $testString = "I have an ' apostrophe" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "`|'" + } + + It "Should Properly Sanitize ’" { + $testString = "I have a *fancy* ’ apostrophe" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "`|’" + } + + It "Should Properly Sanitize [" { + $testString = "I have a left [ bracket" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + # Powershell wants the ` for the |, regex wants the \ for the [... Rage. + $resultString | Should -Match "`|\[" + } + + It "Should Properly Sanitize ]" { + $testString = "I have a right ] bracket" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "`|]" + } + } + + Context "When Provided With a Newline Charater" { + It "Should Properly Sanitize ``n" { + $testString = "I have an + end line" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "`\|n" + } + } + + Context "When Provided With a Character Return Character" { + It "Should Properly Sanitize ``r" { + $testString = "I have a character `r return" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -Match "`\|r" + } + } + + Context "When Provided With a String Which Doesn't Need Any Modification" { + It "Should Make No Changes" { + $testString = "I am a string with no special characters." + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -eq $testString + } + } + Context "When Provided With an empty string" { + It "Should Return an Empty string" { + $testString = "" + + $resultString = ConvertTo-SafeTeamCityMessage $testString + + $resultString | Should -eq $testString + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Copy-ObjectProperties.ps1 b/Modules/Alkami.PowerShell.Common/Public/Copy-ObjectProperties.ps1 new file mode 100644 index 0000000..dfbed3a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Copy-ObjectProperties.ps1 @@ -0,0 +1,28 @@ +function Copy-ObjectProperties { +<# +.SYNOPSIS + Copies properties from one object to another +#> + param( + [Parameter(Position = 0, Mandatory = $true)] + [Object]$SourceObject, + + [Parameter(Position = 1, Mandatory = $true)] + [Object]$DestinationObject, + + [Parameter(Position = 2, Mandatory = $false)] + [string[]]$PropertiesToSkip + ) + + foreach ($property in $SourceObject.PsObject.Properties) { + if (($PropertiesToSkip -notcontains $property.Name) -and + ($null -ne $DestinationObject.PsObject.Properties.Match($property.Name)) -and + ($DestinationObject.PsObject.Properties[$property.Name].TypeNameOfValue -eq $property.TypeNameOfValue) -and + ($DestinationObject.PsObject.Properties[$property.Name].IsSettable)) { + $DestinationObject.PsObject.Properties[$property.Name].Value = $property.Value + } + } + + $DestinationObject +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Find-CertificateByName.ps1 b/Modules/Alkami.PowerShell.Common/Public/Find-CertificateByName.ps1 new file mode 100644 index 0000000..80afa49 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Find-CertificateByName.ps1 @@ -0,0 +1,74 @@ +function Find-CertificateByName { + <# + .SYNOPSIS + Finds and returns a certificate from the certificates stores with a common name. + + .PARAMETER CommonName + The Common Name of the certificate to search for. + + .PARAMETER StoreLocation + The certificate store location. CurrentUser or LocalMachine + + .PARAMETER StoreName + The name of the certificate store to search. + #> + Param( + [Parameter(Mandatory=$true)] + [String] + $CommonName, + [Parameter(Mandatory=$true)] + [ValidateSet("CurrentUser", "LocalMachine")] + [String] + $StoreLocation, + [Parameter(Mandatory=$true)] + [ValidateSet("My", "CA", "Root", "TrustedPeople")] + [String] + $StoreName + ) + + $loglead = (Get-LogLeadName); + + # Get all of the certificates from the specified certificate store. + $storePath = "Cert:\$StoreLocation\$StoreName"; + Write-Verbose "$loglead Searching for certificate with Common Name '$CommonName' in store path '$storePath'"; + [array]$allCerts = (Get-ChildItem -Path $storePath); + + # Find all of the certs that have the common name we are looking for. + $certificates = @(); + foreach($cert in $allCerts) { + # Parse out the common name. + $subjectSplit = $cert.Subject.Split(","); + foreach($ss in $subjectSplit) { + $propertySplit = $ss.Split("="); + if($propertySplit.Count -ne 2) { + continue; + } + + $key = $propertySplit[0].Trim(); + $value = $propertySplit[1].Trim(); + + # If the common name matches the certificate we are looking for, store the cert. + if(($key -eq "CN") -and ($value -eq $CommonName)) { + $certificates += $cert; + break; + } + } + } + + # Return if the certificate could not be found. + if(Test-IsCollectionNullOrEmpty $certificates) { + Write-Warning "$loglead Could not find certificate with Common Name $CommonName"; + return $null; + } + + # Sort the certificates by their issue date to pick the latest issued cert. + $certificates = ($certificates | Sort-Object -Property "NotBefore" -Descending); + + # Write-out all of the certificates that we found. + foreach($cert in $certificates) { + Write-Verbose "$loglead Found certificate with thumbprint $($cert.Thumbprint)" + } + + # Return the top certificate that was found. + return ($certificates | Select-Object -First 1); +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Format-Json.ps1 b/Modules/Alkami.PowerShell.Common/Public/Format-Json.ps1 new file mode 100644 index 0000000..ec53198 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Format-Json.ps1 @@ -0,0 +1,46 @@ +function Format-Json { +<# +.SYNOPSIS + Formats JSON in a nicer format than the built-in ConvertTo-Json does. + +.PARAMETER json + Can pass in either an object or a string representation of a json object. See examples. + +.EXAMPLE + Write-Host ($json | ConvertTo-Json | Format-Json) + +.EXAMPLE + Write-Host ($json | Format-Json) + +.LINK + https://github.com/PowerShell/PowerShell/issues/2736 +#> + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [object]$json + ) + + if ($json.GetType() -ne [string]) { + # Never stop at depth 4 (default) - Max is 100 + $json = (ConvertTo-Json -InputObject $json -Depth 100 -Compress:$false) + } + + $indent = 0; + $lines = $json -Split '\n' + $newLines = @() + + foreach($line in $lines) { + if ($line -match '[\}\]]') { + # This line contains ] or }, decrement the indentation level + $indent-- + } + $newLines += (' ' * $indent * 2) + $line.TrimStart().Replace(': ', ': ') + if ($line -match '[\{\[]') { + # This line contains [ or {, increment the indentation level + $indent++ + } + } + + # the reason for the replaces is because sometimes a comment can just be formatted correctly + return ($newLines -Join "`n").Replace("\u003c","<").Replace("\u003e",">") +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Format-Url.ps1 b/Modules/Alkami.PowerShell.Common/Public/Format-Url.ps1 new file mode 100644 index 0000000..a49286b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Format-Url.ps1 @@ -0,0 +1,41 @@ +function Format-Url { +<# +.SYNOPSIS + Removes preceeding http(s) prefixes and trailing slashes from URLs + +.DESCRIPTION + Removes preceeding http(s) prefixes and trailing slashes from URLs. Does not break down to the host name alone + +.PARAMETER url + [string] The URL to Clean + +.EXAMPLE + Format-Url "https://foo.bar.com/" + +foo.bar.com +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory)] + [String]$url + ) + + $logLead = (Get-LogLeadName) + [Regex]$urlCleanerRegex = "^http(s)?:\/\/|\/$" + + Write-Verbose "$logLead : Cleaning URL $url with regex $urlCleanerRegex" + + if (!($urlCleanerRegex.IsMatch($url.Trim()))) { + + Write-Host "$logLead : URL $url Doesn't Need Cleaning." + return $url + } + + $cleansedUrl = $urlCleanerRegex.Replace($url.Trim(), "") | Select-Object -First 1 + Write-Verbose "$logLead : Cleansed URL is: $cleansedUrl" + + return $cleansedUrl +} + +Set-Alias -name Clean-Url -value Format-Url; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Format-Url.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Format-Url.tests.ps1 new file mode 100644 index 0000000..d6cd76d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Format-Url.tests.ps1 @@ -0,0 +1,45 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Format-Url" { + + Context "Potential Formats" { + + $expectedValue = "my.fake.site.com" + + It "Trims https://" { + + $testString = "https://my.fake.site.com" + Format-Url $testString | Should -Be $expectedValue + } + + It "Trims http://" { + + $testString = "http://my.fake.site.com" + Format-Url $testString | Should -Be $expectedValue + } + + It "Trims trailing forward slashes" { + + $testString = "my.fake.site.com/" + Format-Url $testString | Should -Be $expectedValue + } + + It "Trims Input Parameters" { + + $testString = " https://my.fake.site.com/ " + Format-Url $testString | Should -Be $expectedValue + } + + It "Doesn't Replace Leading H Values in Host" { + + $testString = "http://hb-ip.cartercu.org/" + Format-Url $testString | Should -Be "hb-ip.cartercu.org" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-7ZipPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-7ZipPath.ps1 new file mode 100644 index 0000000..ad4c2b5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-7ZipPath.ps1 @@ -0,0 +1,21 @@ +function Get-7ZipPath { +<# +.SYNOPSIS + Get the path to the 7zip exe +#> + [CmdletBinding()] + Param() + + $paths = @("C:\ProgramData\Alkami\Common\7za.exe", "C:\Program Files\7-Zip\7z.exe") + + foreach($path in $paths) + { + if(Test-Path $path) + { + return $path + } + } + + throw 'Could not find the 7Zip executable on disk'; +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiCredential.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiCredential.ps1 new file mode 100644 index 0000000..042a8d0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiCredential.ps1 @@ -0,0 +1,38 @@ +function Get-AlkamiCredential { +<# +.SYNOPSIS + Creates a new PSCredential object from a given username and password. + +.DESCRIPTION + Get-AlkamiCredential will create a credential for you from a username and password, converting a password stored as a String into a SecureString. + +.OUTPUTS + System.Management.Automation.PSCredential + +.EXAMPLE + Get-AlkamiCredential -User ENTERPRISE\picard -Password 'earlgrey' + Creates a new credential object for Captain Picard. +#> + [CmdletBinding()] + [OutputType([Management.Automation.PSCredential])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams","")] + param( + [Alias('User')] + [string] + # The username. Beginning with Carbon 2.0, this parameter is optional. Previously, this parameter was required. + $UserName, + + [Parameter(Mandatory=$true,ValueFromPipeline=$true)] + # The password. Can be a `[string]` or a `[System.Security.SecureString]`. + $Password + ) + + if($Password -is [string]) { + $Password = ConvertTo-SecureString -AsPlainText -Force -String $Password + } elseif($Password -isnot [securestring]) { + Write-Error "Value for Password parameter must be a [string] or [System.Security.SecureString]. You passed a [$($Password.GetType())]." + return + } + + return New-Object 'Management.Automation.PsCredential' $UserName,$Password +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.ps1 new file mode 100644 index 0000000..5d6bfca --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.ps1 @@ -0,0 +1,42 @@ +function Get-AlkamiInstallationDrive { + <# + .SYNOPSIS + Get the drive letter (and colon) that describes where Alkami Platform software is installed + + .DESCRIPTION + Using Environment Variables, determine where the Alkami Platform software is installed. + If present and in the correct format, ENV:ALKAMI_INSTALLATION_DRIVE is used. Otherwise, + ENV:SystemDrive is used. +#> + [CmdletBinding()] + [OutputType([string])] + param ( + ) + + $logLead = Get-LogLeadName + + # PATTERN EXPLAINER + # ^ - beginning of string + # [A-Za-z] + # [] - a list or set of characters + # A-Za-z - all letters from A to Z and a to z + # :? - a colon, maybe + # $ - end of string + # + # ^[A-Za-z]:$ - one alphabetical letter, one colon, nothing before, nothing after + # + $driveLetterPattern = "^[A-Za-z]:?$" + $keyName = 'ALKAMI_INSTALLATION_DRIVE' + + $alkamiInstallationDrive = Get-EnvironmentVariable -Name $keyName + + if ($alkamiInstallationDrive -cnotmatch $driveLetterPattern) { + if (-NOT (Test-StringIsNullOrWhitespace -Value $alkamiInstallationDrive)) { + # Use of this will almost surely include `Join-Path` - which deals with having or not having a colon + Write-Warning "$logLead : NON-NULL Value for '$keyName' is not a drive letter with or without a colon. Value is [$alkamiInstallationDrive]" + } + $alkamiInstallationDrive = Get-EnvironmentVariable -Name "SystemDrive" + } + + return $alkamiInstallationDrive +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.tests.ps1 new file mode 100644 index 0000000..4a4dcff --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiInstallationDrive.tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Main" { + + $FAKE_ALKAMI_INSTALLATION_DRIVE = "A:" + $FAKE_SYSTEM_DRIVE = "T:" + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -MockWith {} + + Context "Happy_Path" { + It "Should_call_Get-LogLeadName" { + $result = Get-AlkamiInstallationDrive + Assert-MockCalled -CommandName Get-LogLeadName -Times 1 -Scope It + } + + It "Should_get_ALKAMI_INSTALLATION_DRIVE_env_var" { + Mock -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -ParameterFilter { $Name -eq 'ALKAMI_INSTALLATION_DRIVE' } -MockWith { return $FAKE_ALKAMI_INSTALLATION_DRIVE } + + $result = Get-AlkamiInstallationDrive + Assert-MockCalled -CommandName Get-EnvironmentVariable -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'ALKAMI_INSTALLATION_DRIVE' } -Times 1 -Scope It + Assert-MockCalled -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'SystemDrive' } -Times 0 -Exactly -Scope It + $result | Should -Be $FAKE_ALKAMI_INSTALLATION_DRIVE + } + + } + Context "Unhappy_Path" { + It "Uses_SystemDrive_When_ALKAMI_INSTALLATION_DRIVE_is_nullorempty" { + Mock -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -ParameterFilter { $Name -eq 'ALKAMI_INSTALLATION_DRIVE' } -MockWith { return $null } + Mock -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -ParameterFilter { $Name -eq 'SystemDrive' } -MockWith { return $FAKE_SYSTEM_DRIVE } + + $result = Get-AlkamiInstallationDrive + Assert-MockCalled -CommandName Get-EnvironmentVariable -Times 2 -Exactly -Scope It + Assert-MockCalled -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'SystemDrive' } -Times 1 -Exactly -Scope It + $result | Should -Be $FAKE_SYSTEM_DRIVE + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiManifestFilename.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiManifestFilename.ps1 new file mode 100644 index 0000000..ecb123a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AlkamiManifestFilename.ps1 @@ -0,0 +1,13 @@ +function Get-AlkamiManifestFilename { +<# +.SYNOPSIS + Get the name of the AlkamiManifest so we consistently consume it in case we want to change it. +#> + [CmdletBinding()] + [OutputType([System.String])] + [Obsolete("Usages of this file and surrounding files should be converted to use Alkami.PowerShell.Configuration\Get-PackageManifest")] + Param() + + return "AlkamiManifest.xml"; +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.ps1 new file mode 100644 index 0000000..4043190 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.ps1 @@ -0,0 +1,32 @@ +function Get-AvailabilityZone { +<# +.SYNOPSIS + Returns the availability zone name of a server in AWS, or otherwise $null. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName) + + Import-AWSModule # EC2 + + if($ComputerName -eq "localhost" -or $ComputerName -eq "." -or $ComputerName -eq (hostname)) { + return Get-CurrentInstanceAvailabilityZone + } else { + $filter =@( + @{ + name='tag:alk:hostname'; + values="$ComputerName" + } + ) + Write-Verbose "$logLead : Getting instance" + $instance = (Get-EC2Instance -Filter $filter).Instances | Select-Object -First 1 + $placementData = $instance.Placement + $az = $placementData.AvailabilityZone + return $az + } + +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.tests.ps1 new file mode 100644 index 0000000..e605268 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AvailabilityZone.tests.ps1 @@ -0,0 +1,89 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AvailabilityZone" { + # Load up AWSPowerShell for Mocking if Available + $awsPowerShellLoaded = $false + if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) { + + Import-AWSModule # EC2 + $awsPowerShellLoaded = $true + } else { + + Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute." + } + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Invoke-WebRequest {} + Mock -CommandName Get-CurrentInstanceAvailabilityZone -ModuleName $moduleForMock -MockWith { + return $null + } + + It "Returns Null When Not Run in AWS" { + if (!($awsPowerShellLoaded)) { + Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed" + continue; + } + Mock -CommandName Write-Warning -MockWith { } -ModuleName $moduleForMock + Mock -ModuleName Alkami.DevOps.Common Test-IsAws { return $false } + Get-AvailabilityZone | Should -BeNullOrEmpty + } + } + + Context "When Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-EC2Instance { + return @{ + Instances = @( + @{ + Placement = + @{ + AvailabilityZone = "us-fake-1a" + } + } + ) + } + } + Mock -ModuleName $moduleForMock Get-CurrentInstanceAvailabilityZone { + return "us-fake-2b" + } + + It "Not-localhost Calls Get-EC2Instance and Returns the Instance AvailabilityZone" { + + $testResult = Get-AvailabilityZone -ComputerName "fakeweb01" + Assert-MockCalled -ModuleName $moduleForMock Get-EC2Instance -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-1a" + } + It "Localhost Calls Get-CurrentInstanceAvailabilityZone" { + $testResult = Get-AvailabilityZone -ComputerName "localhost" + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstanceAvailabilityZone -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-2b" + } + It "Explicit local hostname Calls Get-CurrentInstanceAvailabilityZone" { + $localHostname = (hostname) + $testResult = Get-AvailabilityZone -ComputerName $localHostname + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstanceAvailabilityZone -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-2b" + } + It "No ComputerName(defaults to localhost) Calls Get-CurrentInstanceAvailabilityZone" { + + $testResult = Get-AvailabilityZone + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstanceAvailabilityZone -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-2b" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.ps1 new file mode 100644 index 0000000..d61bcd0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.ps1 @@ -0,0 +1,36 @@ +function Get-AwsSettings { +<# + .SYNOPSIS + Get AWS settings (region) from a specific server. Assumes the server is accessible. + .PARAMETER ServerToTest + Server to remote into to get AWS details from. + .PARAMETER ProfileName + AWS Profile to use. +#> + [CmdletBinding()] + param( + $ServerToTest, + $ProfileName + ) + $logLead = (Get-LogLeadName); + $scriptBlock = { + + $currentRegion = Get-CurrentInstanceRegion + + $returnSettings = @{ + "Region" = $currentRegion + } + + return $returnSettings + } + + Write-Host "$logLead : Attempting to Get Aws Settings from $ServerToTest." + $serverInfo = Invoke-Command -ScriptBlock $scriptBlock -ComputerName $ServerToTest + + $settings = @{ + "Region" = $serverInfo.Region + "Profile" = $ProfileName + } + + return $settings +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.tests.ps1 new file mode 100644 index 0000000..3214ada --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-AwsSettings.tests.ps1 @@ -0,0 +1,46 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AwsSettings" { + + Context "When Called" { + + Mock -ModuleName $moduleForMock Invoke-Command { + $returnSettings = @{ + "Region" = "mordor-west-1" + } + + return $returnSettings + } + Mock -ModuleName $moduleForMock Write-Host {} + + $profileName = "unitTestProfile" + $serverName = "myFakeServer" + + It "Returns Settings Object"{ + $settings = Get-AwsSettings -ServerToTest $serverName -ProfileName $profileName + Assert-MockCalled -ModuleName $moduleForMock Invoke-Command + + $settings | Should -Not -Be $null + } + + It "Returns Region"{ + $settings = Get-AwsSettings -ServerToTest $serverName -ProfileName $profileName + Assert-MockCalled -ModuleName $moduleForMock Invoke-Command + + $settings.Region | Should -Be "mordor-west-1" + } + + It "Returns ProfileName"{ + $settings = Get-AwsSettings -ServerToTest $serverName -ProfileName $profileName + Assert-MockCalled -ModuleName $moduleForMock Invoke-Command + + $settings.Profile | Should -Be $profileName + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CPUUsage.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CPUUsage.ps1 new file mode 100644 index 0000000..bfc8feb --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CPUUsage.ps1 @@ -0,0 +1,63 @@ +function Get-CPUUsage { + <# + .SYNOPSIS + Returns the current CPU utilization of the CPU, or of individual services. + + .PARAMETER serviceName + A string array of service names to query for CPU utilization. + Pass in service names, or "*" for the total CPU usage. + Results are returned in the same order that they are passed in. + + .PARAMETER numSamples + The number of CPU samples to query for. + + .PARAMETER interval + The time interval between CPU samples in seconds. + + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string[]]$serviceName = @("*"), + [Parameter(Mandatory = $false)] + [int]$numSamples = 1, + [Parameter(Mandatory = $false)] + [int]$interval = 1 + ) + + $queries = @(); + foreach($name in $serviceName) { + if($name -eq "*") { + $queries += "\Processor(_Total)\% Processor Time"; + } else { + $queries += "\Process($name)\% Processor Time"; + } + } + + $samples = Get-Counter -Counter $queries -MaxSamples $numSamples -SampleInterval $interval; + + # Set all counter sums to 0. + $sums = New-Object int[] $queries.Count; + for($i = 0; $i -lt $queries.Count; $i++) { + $sums[$i] = 0; + } + # Sum all of the CPU usage values, for each counter query. + foreach($sample in $samples) { + for($i = 0; $i -lt $queries.Count; $i++) { + $sums[$i] += $sample.CounterSamples[$i].CookedValue; + } + } + # Divide usage by number of samples to get average. + for($i = 0; $i -lt $queries.Count; $i++) { + $average = $sums[$i] / $numSamples; + + # Clamp the CPU usage between 0-100 just in case. + $average = [Math]::Min(100, $average); + $average = [Math]::Max(0, $average); + + # Store the average back out to sums in the name of re-use. + $sums[$i] = $average + } + + return $sums; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ChocolateyInstallPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ChocolateyInstallPath.ps1 new file mode 100644 index 0000000..ccdc87c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ChocolateyInstallPath.ps1 @@ -0,0 +1,20 @@ +function Get-ChocolateyInstallPath { +<# +.SYNOPSIS + Get the path to the root of the chocolatey install folder +#> + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + $envChocolateyInstallValue = $env:ChocolateyInstall + + if ($null -eq $envChocolateyInstallValue) { + $envChocolateyInstallValue = (Join-Path -Path (Join-Path -Path $env:SystemDrive -ChildPath "ProgramData") -ChildPath "chocolatey") + } + + Write-Verbose "$logLead : Found chocolatey install path [$envChocolateyInstallValue]" + return $envChocolateyInstallValue +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.ps1 new file mode 100644 index 0000000..e27812c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.ps1 @@ -0,0 +1,45 @@ +function Get-CoalescedStringValue { + + <# + .SYNOPSIS + Simple null coalesce function for string values. Null, empty, and whitespace strings will return the second provided value + .DESCRIPTION + Simple null coalesce function for string values. Null, empty, and whitespace strings will return the second provided value + .PARAMETER ValueA + Value to test for null/empty equivalency. This value is returned it is not equivalent to null, empty, or whitespace only. + .PARAMETER ValueB + Value to return if ValueA is equivalent to null, empty, or whitespace + .NOTES + Similar in function to Test-IsNull, but written to begin moving away from that poorly named and convoluted function, focusing on string values + #> + + [CmdletBinding()] + [OutputType([System.String])] + param ( + [Parameter(Mandatory=$false)] + [Alias("TestValue")] + [string]$ValueA = $null, + + [Parameter(Mandatory=$false)] + [Alias("FallbackValue")] + [string]$ValueB + ) + + $logLead = Get-LogLeadName + $valueAIsNullEmptyOrWhitespace = Test-StringIsNullOrWhitespace -Value $ValueA + + if ($valueAIsNullEmptyOrWhitespace) { + + $valueBIsNullEmptyOrWhitespace = Test-StringIsNullOrWhitespace -Value $ValueB + if ($valueBIsNullEmptyOrWhitespace) { + + $nullValueBWarningMessage = "$logLead : Value B is null, empty, or whitespace. ValueB will be returned; however, assumptions in the calling code should be checked for " + ` + "appropriate handling. Note that PowerShell will coerce a null value to an empty string." + Write-Warning $nullValueBWarningMessage + } + + return $ValueB + } + + return $ValueA +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.tests.ps1 new file mode 100644 index 0000000..73a419d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CoalescedStringValue.tests.ps1 @@ -0,0 +1,77 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CoalescedStringValue" { + + Context "Logic" { + + It "Returns ValueA if it Is Not Null, Empty, or WhiteSpace" { + + $valueA = "I am the very model" + $valueB = "of a modern Major-General" + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueA + } + + It "Returns ValueB if ValueA is Null" { + + $valueA = $null + $valueB = "I've information vegetable, animal, and mineral" + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueB + } + + It "Returns ValueB if ValueA is Whitespace" { + + $valueA = " " + $valueB = "I know the kings of England, and I quote the fights Historical" + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueB + } + + It "Returns ValueB if ValueA is an Empty String" { + + $valueA = "" + $valueB = "From Marathon to Waterloo, in order categorical" + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueB + } + } + + Context "Edge Cases" { + + It "Returns an empty string and Writes a Warning if ValueA and ValueB are Both Null" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + $valueA = $null + $valueB = $null + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be ([String]::Empty) + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Value B is null, empty, or whitespace." } + } + + It "Returns ValueB and Writes a Warning if ValueA is null and ValueB is Whitespace" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + $valueA = $null + $valueB = " " + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueB + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Value B is null, empty, or whitespace." } + } + + It "Returns ValueB and Writes a Warning if ValueA is null and ValueB is an Empty String" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + + $valueA = $null + $valueB = "" + Get-CoalescedStringValue -ValueA $valueA -ValueB $valueB | Should Be $valueB + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -Match "Value B is null, empty, or whitespace." } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ConnectionString.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ConnectionString.ps1 new file mode 100644 index 0000000..d09250d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ConnectionString.ps1 @@ -0,0 +1,49 @@ +function Get-ConnectionString { +<# +.SYNOPSIS + Returns a ConnectionString by name from the specified config file. Filepath defaults to the 64 bit machine config. +#> + param ( + [Parameter(Mandatory = $true)] + [string]$name, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$filePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName); + + # If a computername was provided, modify the filepath to be a UNC path. + if((![string]::IsNullOrWhiteSpace($ComputerName)) -and ($ComputerName -ne "localhost")) { + $filePath = (Get-UncPath -filePath $filePath -ComputerName $ComputerName); + } + + if (!(Test-Path -PathType Leaf -Path $filePath)) { + Write-Warning ("$logLead : Could not find a file at {0}. Execution cannot continue" -f $filePath) + return $null + } + + $xml = Read-XMLFile $filePath + + Write-Verbose "$logLead : Looking for appSettings Node" + [array]$connectionStringNode = $xml.SelectNodes("//connectionStrings") + if (($null -eq $connectionStringNode) -or ($connectionStringNode.Count -eq 0)) { + Write-Warning ("$logLead : Could not Find appSettings Node in {0}" -f $filePath) + return $null + } + + Write-Verbose ("$logLead : Looking for Child Nodes with Key {0}" -f $name) + [array]$targetNodes = $connectionStringNode.ChildNodes | Where-Object {$_.Name -eq $name} + if (($null -eq $targetNodes) -or ($targetNodes.Count -eq 0)) { + Write-Warning ("$logLead : Could not Find ConnectionString Add Node with Name {0} in {1}" -f $name, $filePath) + return $null + } elseif ($targetNodes.Count -gt 1) { + Write-Warning ("$logLead : Found {0} connection strings with name {1}. This is incorrect, and only the first will be returned" -f $targetNodes.Count, $name) + } + + return ($targetNodes | Select-Object -First 1).connectionString; +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.ps1 new file mode 100644 index 0000000..bdab0a8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.ps1 @@ -0,0 +1,34 @@ +function Get-CurrentInstance { +<# +.SYNOPSIS + Gets the Current Instance Object +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + Import-AWSModule # EC2 + + if ( (Test-IsAws) -eq $false ) { + + Write-Warning "$logLead : This function can only be executed on an AWS server" + return $null + } + + try { + $instance = Get-EC2Instance -InstanceId (Get-CurrentInstanceId) + + if (($null -eq $instance) -or ($null -eq $instance.Instances)) { + Write-Warning "$logLead : The Instance Was Null!" + return $null + } + + return (([array]$instance.Instances) | Select-Object -First 1) + } catch { + Write-Warning ("$logLead : Could Not Pull Current Instance Due to Error: {0}" -f $_.Exception.Message) + return $null + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.tests.ps1 new file mode 100644 index 0000000..a40e93b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstance.tests.ps1 @@ -0,0 +1,97 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CurrentInstance" { + + $TestIsAwsWarning = "This function can only be executed on an AWS server" + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-CurrentInstanceId {} + Mock -ModuleName $moduleForMock Get-EC2Instance {} + + Get-CurrentInstance -WarningAction SilentlyContinue + + It "Calls Test-IsAws" { + + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope Context + } + + It "Writes a Warning" { + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -ParameterFilter {$Message -match $TestIsAwsWarning} -Times 1 -Exactly -Scope Context + } + + It "Does not call Get-CurrentInstanceId" { + + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstanceId -Times 0 -Exactly -Scope Context + } + + It "Does not call AWS APIs" { + + Assert-MockCalled -ModuleName $moduleForMock Get-EC2Instance -Times 0 -Exactly -Scope Context + } + + It "Returns null" { + Get-CurrentInstance | Should -BeNullOrEmpty + } + } + + Context "When Running in AWS" { + + $expectedInstance = @{ + InstanceId = "i-fake001" + ImageId = "ami-fakeImage01" + InstanceType = "mock.xFake" + } + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-CurrentInstanceId { + return "i-fake001" + } + Mock -ModuleName $moduleForMock Get-EC2Instance { + return @{ + Instances = @( + @{ + InstanceId = "i-fake001" + ImageId = "ami-fakeImage01" + InstanceType = "mock.xFake" + }, + @{ + InstanceId = "i-fake002" + ImageId = "ami-fakeImage02" + InstanceType = "mock.xFake" + } + ) + } + } + It "Returns the Instance" { + + $testResult = Get-CurrentInstance + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstanceId -Times 1 -Exactly -Scope It + + (Compare-Object $expectedInstance.Values $testResult.Values) | Should -BeNullOrEmpty + } + It "Calls Test-IsAws" { + + Get-CurrentInstance + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope It + } + + It "Does Not Write Warning" { + + Get-CurrentInstance + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -ParameterFilter {$Message -match $TestIsAwsWarning} -Times 0 -Exactly -Scope It + } + + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.ps1 new file mode 100644 index 0000000..a52c087 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.ps1 @@ -0,0 +1,26 @@ +function Get-CurrentInstanceAvailabilityZone { + <# + .SYNOPSIS + Gets the availability zone of the current instance. + #> + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName) + + $endpoint = "/meta-data/placement/availability-zone" + + if (!(Test-IsAws)) { + + Write-Warning "$logLead : This function can only be executed on an AWS server" + return $null + } + + Write-Verbose "$logLead : Querying $endpoint for availability zone name." + $azQuery = (Get-InstanceMetadata -Endpoint $endpoint) + if($azQuery.StatusCode -eq "200") { + return $azQuery.Content + } + + throw "Invoke-WebRequest failed with status code $($azQuery.StatusCode)" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.tests.ps1 new file mode 100644 index 0000000..6d12eb7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceAvailabilityZone.tests.ps1 @@ -0,0 +1,67 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CurrentInstanceAvailabilityZone" { + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata {} + + Get-CurrentInstanceAvailabilityZone -WarningAction SilentlyContinue + + It "Calls Test-IsAws" { + + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope Context + } + + It "Writes a Warning" { + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope Context + } + + It "Returns Without Calling AWS APIs" { + + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -ParameterFilter {$Endpoint -Match $azEndpointPath } -Times 0 -Exactly -Scope Context + } + } + + Context "When Running in AWS" { + + $azEndpointPath = "meta-data/placement/availability-zone" + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata { + + return (New-Object PSObject -Property @{ + "StatusCode" = "200"; + "Content" = "us-fake-1a"; + } ) + } + + It "Returns the Instance AvailabilityZone" { + + $testResult = Get-CurrentInstanceAvailabilityZone + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -ParameterFilter {$Endpoint -Match $azEndpointPath } -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-1a" + } + + It "Throws if not success" { + Mock -ModuleName $moduleForMock Get-InstanceMetadata { + return (New-Object PSObject -Property @{ + "StatusCode" = "404"; + "Content" = "Not found"; + } ) + } + + {Get-CurrentInstanceAvailabilityZone} | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.ps1 new file mode 100644 index 0000000..3aaf26f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.ps1 @@ -0,0 +1,35 @@ +function Get-CurrentInstanceId { + <# + .SYNOPSIS + Gets the Current Instance ID + #> + + [CmdletBinding()] + [OutputType([System.String])] + Param() + + $logLead = (Get-LogLeadName); + + $endpoint = "/meta-data/instance-id" + + if ( (Test-IsAws) -eq $false ) { + + Write-Warning "$logLead : This function can only be executed on an AWS server" + return $null + } + + try { + $instanceId = (Get-InstanceMetadata -Endpoint $endpoint).Content + + if ($null -eq $instanceId) { + Write-Warning ("$logLead : Instance ID Came Back Null") + } else { + Write-Verbose ("$logLead : Instance ID Read as {0}" -f $instanceId) + } + + return $instanceId + } catch { + Write-Warning ("$logLead : AWS MetaData Check Returned Error: {0}" -f $_.Exception.Message) + return $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.tests.ps1 new file mode 100644 index 0000000..890c3a5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceId.tests.ps1 @@ -0,0 +1,75 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CurrentInstanceId" { + + $TestIsAwsWarning = "This function can only be executed on an AWS server" + $imdsEndpointPath = "meta-data/instance-id" + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata {} + + Get-CurrentInstanceId + + It "Calls Test-IsAws" { + + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope Context + } + + It "Writes a Warning" { + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly ` + -ParameterFilter { $Message -match $TestIsAwsWarning } -Scope Context + } + + It "Returns Without Calling AWS APIs" { + #This works because the Invoke-WebRequest in Test-IsAws is in another namespace + #If needed, we could add parameter filter like $azEndpointPath below + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -Times 0 -Exactly -Scope Context + } + + It "Returns Null" { + Get-CurrentInstanceId | Should -BeNullOrEmpty + } + } + + Context "When Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata { + + return (New-Object PSObject -Property @{ + "StatusCode" = "200"; + "Content" = "i-fake001"; + } ) + } + + It "Returns instance id" { + $testResult = Get-CurrentInstanceId + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -Times 1 -Exactly ` + -ParameterFilter {$Endpoint -match $imdsEndpointPath} -Scope It + + $testResult | Should -Be "i-fake001" + } + It "Calls Test-IsAws" { + + Get-CurrentInstanceId + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope It + } + + It "Does Not Write Warning" { + + Get-CurrentInstanceId + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -ParameterFilter {$Message -match $TestIsAwsWarning} -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.ps1 new file mode 100644 index 0000000..9311414 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.ps1 @@ -0,0 +1,27 @@ +function Get-CurrentInstanceRegion { + <# + .SYNOPSIS + Uses IMDS metadata to retrieve the instance region. + #> + [CmdletBinding()] + [OutputType([System.String])] + param() + + $logLead = (Get-LogLeadName) + + $endpoint = "/dynamic/instance-identity/document" + + if (!(Test-IsAws)) { + + Write-Warning "$logLead : This Function Can Only Be Called on an AWS Server" + return + } + + Write-Verbose "$logLead : Getting Current Instance" + $identityDocumentJson = (Get-InstanceMetadata -Endpoint $endpoint) + + $region = ($identityDocumentJson | ConvertFrom-Json | Select-Object Region).Region + + Write-Verbose "$logLead : Region Read as $region" + return $region +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.tests.ps1 new file mode 100644 index 0000000..5da7f85 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceRegion.tests.ps1 @@ -0,0 +1,63 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CurrentInstanceRegion" { + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata {} + + Get-CurrentInstanceRegion -WarningAction SilentlyContinue + + It "Writes a Warning" { + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope Context + } + + It "Returns Without Calling AWS APIs" { + + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -Times 0 -Exactly -Scope Context + } + } + + Context "When Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-InstanceMetadata { + + return (New-Object PSObject -Property @{ + "privateIp" = "10.16.72.103"; + "devpayProductCodes" = $null; + "marketplaceProductCodes" = $null; + "version" = "2017-09-30"; + "instanceId" = "i-0ecc6833047799e3b"; + "billingProducts" = "bp-6ba54002"; + "instanceType" = "m5.2xlarge"; + "kernelId" = $null; + "ramdiskId" = $null; + "availabilityZone" = "us-fake-1a"; + "accountId" = "790953160341"; + "architecture" = "x86_64"; + "imageId" = "ami-7b454e04"; + "pendingTime" = "2018-10-03T20:19:27Z"; + "region" = "us-fake-1"; + } | ConvertTo-JSON) + } + + It "Returns the Instance Region" { + + $testResult = Get-CurrentInstanceRegion + Assert-MockCalled -ModuleName $moduleForMock Get-InstanceMetadata -Times 1 -Exactly -Scope It + + $testResult | Should -Be "us-fake-1" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.ps1 new file mode 100644 index 0000000..09d87ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.ps1 @@ -0,0 +1,54 @@ +function Get-CurrentInstanceTags { +<# +.SYNOPSIS + Gets Tags from the Current EC2 Instance +.PARAMETER tagName +[string] Optional. When specified, only returns the tag with the matching Name +.PARAMETER ValueOnly +[switch] Optional. When set, only returns the value(s) instead of the key/value pair(s) +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$tagName, + + [Parameter(Mandatory = $false)] + [Alias("ValueOnly")] + [switch]$onlyReturnValue + ) + + $logLead = (Get-LogLeadName); + + if ( (Test-IsAws) -eq $false ) { + + Write-Warning "$logLead : This function can only be executed on an AWS server" + return $null + } + + Write-Verbose "$logLead : Getting Current Instance" + $instance = Get-CurrentInstance + $tags = $instance.Tags + Write-Verbose ("$logLead : Found {0} Tags" -f $tags.Count) + + if (!([String]::IsNullOrEmpty($tagName))) + { + $matchingTag = ($tags | Where-Object {$_.Key -eq $tagName} | Select-Object -First 1) + + if ($onlyReturnValue.IsPresent) + { + Write-Verbose "$logLead : Returning value only" + return $matchingTag.Value + } + + return $matchingTag + } + + if ($onlyReturnValue.IsPresent) + { + Write-Verbose "$logLead : Returning Values only" + return ($tags | Select-Object -ExpandProperty "Value") + } + + return $tags +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.tests.ps1 new file mode 100644 index 0000000..d9cf976 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-CurrentInstanceTags.tests.ps1 @@ -0,0 +1,82 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-CurrentInstanceTags" { + + $TestIsAwsWarning = "This function can only be executed on an AWS server" + + Context "When Not Running in AWS" { + + Mock -ModuleName $moduleForMock Test-IsAws { return $false } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-CurrentInstance {} + + Get-CurrentInstanceTags -WarningAction SilentlyContinue + + It "Calls Test-IsAws" { + + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope Context + } + + It "Writes a Warning" { + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -ParameterFilter {$Message -match $TestIsAwsWarning} -Times 1 -Exactly -Scope Context + } + + It "Does not call Get-CurrentInstance" { + + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstance -Times 0 -Exactly -Scope Context + } + + It "Returns null" { + Get-CurrentInstance | Should -BeNullOrEmpty + } + } + + Context "When Running in AWS" { + + $expectedValues = @( + @{Key="Tag1";Value="Value1"}, + @{Key="Tag2";Value="Value2"}, + @{Key="Tag3";Value="Value3"} + ) + + Mock -ModuleName $moduleForMock Test-IsAws { return $true } + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Get-CurrentInstance { + return @{ + InstanceId = "i-fake001" + Tags = @( + @{Key="Tag1";Value="Value1"}, + @{Key="Tag2";Value="Value2"}, + @{Key="Tag3";Value="Value3"} + ) + } + } + It "Returns the Tags" { + + $testResult = Get-CurrentInstanceTags + Assert-MockCalled -ModuleName $moduleForMock Get-CurrentInstance -Times 1 -Exactly -Scope It + + (Compare-Object -ReferenceObject $expectedValues -DifferenceObject $testResult) | Should -BeNullOrEmpty + } + It "Calls Test-IsAws" { + + Get-CurrentInstanceTags + Assert-MockCalled -ModuleName $moduleForMock Test-IsAws -Times 1 -Exactly -Scope It + } + + It "Does Not Write Warning" { + + Get-CurrentInstanceTags + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -ParameterFilter {$Message -match $TestIsAwsWarning} -Times 0 -Exactly -Scope It + } + + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-DefaultLog4NetPathForPackage.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-DefaultLog4NetPathForPackage.ps1 new file mode 100644 index 0000000..7b8f776 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-DefaultLog4NetPathForPackage.ps1 @@ -0,0 +1,144 @@ +Function Get-DefaultLog4NetPathForPackage { +<# + +.SYNOPSIS + Return a [string] path to the most environment-specific log4net.config file available in a base path or $null + +.DESCRIPTION + File path is determined by PackageName and SourcePath, which will usually be a TeamCity Agent parameter, but may + be a local folder for testing or manual execution. + Where EnvironmentName > EnvironmentType > base (Production) default: + Given an EnvironmentName, or Designation, if a default log4net config file exists with that Designation as its suffix, + returns path to that file. + Given an EnvironmentType (other than Production), if a default log4net config file exists with that EnvironmentType as its + suffix, returns path to that file. + Given neither of the above, or if a matching file does not exist in the 'config-defaults' repo referenced by SourcePath + for prior cases, return path to non-suffixed log4net.config for given PackageName, if it exists. + Otherwise, returns $null. + +.PARAMETER SourcePath + [string] Path to the '/config-defaults/log4net' folder, probably partially derived from TeamCity Agent or Build parameters + +.PARAMETER PackageName + [string] The package name, as it exists in folder format on-disk, for the Chocolatey package needing a default log4net.config file + +.PARAMETER EnvironmentType + [string] Taken from machine.config AppSetting 'Environment.Type'. Example: "Staging" or "Production" + +.PARAMETER EnvironmentName + [string] The smallest form of the environment(pod,lane,box,designation) name;can be found in 'alk:pod', 'alk:lane', etc. Example: NI1, or 12. + +.PARAMETER IsReliableService + [switch] Boolean to determine if the package is a Reliable Service package. This changes the path where Log4Net defaults will look. + +.OUTPUTS + [string] Path to most environment-specific log4net default available for parameters given, or $null + +.EXAMPLE + Get-DefaultLog4NetPathForPackage -SourcePath "C:\AWSBuildAgentB1\work\blah\config-defaults\log4net" -PackageName "Alkami.MicroServices.Alerts.Service.Host" -EnvironmentName "SDKStg" -EnvironmentType "Staging" + +Will return (if it exists): C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config.SDKStg +Else, will return (if it exists): +C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config.staging +Else, will return (if it exists): +C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config +Else, will return +$null + +.EXAMPLE + Get-DefaultLog4NetPathForPackage -SourcePath "C:\AWSBuildAgentB1\work\blah\config-defaults\log4net" -PackageName "Alkami.MicroServices.Alerts.Service.Host" + +Will return (if it exists): C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config + +.EXAMPLE + Get-DefaultLog4NetPathForPackage -SourcePath "C:\AWSBuildAgentB1\work\blah\config-defaults\log4net" -PackageName "Alkami.MicroServices.Alerts.Service.Host" -EnvironmentName "SDKStg" + +Will return (if it exists): C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config.SDKStg + +.EXAMPLE + Get-DefaultLog4NetPathForPackage -SourcePath "C:\AWSBuildAgentB1\work\blah\config-defaults\log4net" -PackageName "Alkami.MicroServices.Alerts.Service.Host" -EnvironmentType "Staging" + +Will return (if it exists): C:\AWSBuildAgentB1\work\blah\config-defaults\log4net\ProgramData\chocolatey\lib\Alkami.MicroServices.Alerts.Service.Host\tools\log4net.config.staging +#> +[CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$SourcePath, + [Parameter(Mandatory = $true)] + [Alias('ElementName','ChocoPackageName')] + [string]$PackageName, + [Parameter()] + [string]$EnvironmentType = "Production", + [Parameter()] + [Alias('Pod','Lane','Box','Designation')] + [string]$EnvironmentName, + [Parameter(Mandatory = $false)] + [switch]$IsReliableService + ) + $logLead = Get-LogLeadName + Write-Host ("Find log4net default for: {0} : {1}" -f $EnvironmentType, $EnvironmentName) + $baseFilter = "log4net.config" + + # {0} => Drive and path prefix, like %checkoutDir% or %systemDrive% sort of thing. + # {1} => package name + $agentPathTemplate = "{0}\ProgramData\chocolatey\lib\{1}\tools" + if($IsReliableService) { + # Reliable Services have a different folder structure. + $agentPathTemplate = "{0}\ProgramData\chocolatey\lib\{1}\svc\Code" + } + # {0} => baseline filename - log4net.config + # {1} => EnvironmentName or EnvironmentType filename suffix - "staging" or "production" or "NI1" or "12" + $filenameTemplate = "{0}.{1}" + + # Determine if it's a load test environment, and override the environment type from what is in the environment. + # In load test environments the environment type is generally Staging and we can't use it. If the ltm environment log4net cannot be found it will fall through to the production config. + $isLoadTestEnvironment = (Test-IsLoadTestEnvironment -EnvironmentName $EnvironmentName) + if($isLoadTestEnvironment) { + $EnvironmentType = "ltm" + } + + $filters = @{} + $filters["base"] = $baseFilter + $filters["podOrLane"] = $EnvironmentName + $filters["environmentType"] = ($EnvironmentType.ToLower()) + + $packageLog4NetFolderPath = ($agentPathTemplate -f $SourcePath, $PackageName) + + # Select the environment name level config if it exists. + if ([string]::IsNullOrEmpty($filters["podOrLane"])) { + Write-Host "$logLead : No environment name/designation specified" + } else { + $designationSpecificFilename = $filenameTemplate -f $filters["base"], $filters["podOrLane"] + $designationSpecificPath = Join-Path -Path $packageLog4NetFolderPath -ChildPath $designationSpecificFilename + if (($null -ne $designationSpecificPath) -and (Test-Path $designationSpecificPath)) { + Write-Host ("$logLead : Found file for {0}. Selecting {1}" -f $filters["podOrLane"], $designationSpecificFilename) + return $designationSpecificPath + } else { + Write-Host "$logLead : $designationSpecificPath not found" + } + } + + # Select the environment type level config if it exists. + if ([string]::IsNullOrEmpty($filters["environmentType"]) -or $filters["environmentType"] -eq "Production") { + Write-Host "$logLead : No environment type or Production specified" + } else { + $environmentSpecificFilename = $filenameTemplate -f $filters["base"], $filters["environmentType"] + $environmentSpecificPath = Join-Path -Path $packageLog4NetFolderPath -ChildPath $environmentSpecificFilename + if (($null -ne $environmentSpecificPath) -and (Test-Path $environmentSpecificPath)) { + Write-Host ("$logLead : Found file for {0}. Selecting {1}" -f $filters["podOrLane"], $environmentSpecificFilename) + return $environmentSpecificPath + } else { + Write-Host "$logLead : $environmentSpecificPath not found" + } + } + + # Otherwise default to the production log4net configs. + $baseProdDefaultPath = Join-Path -Path $packageLog4NetFolderPath -ChildPath $filters["base"] + if (Test-Path $baseProdDefaultPath) { + Write-Host ("$logLead : Found Production log4net file. Selecting {0}" -f $filters["base"]) + return $baseProdDefaultPath + } else { + Write-Host "$logLead : $baseProdDefaultPath not found" + return $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-DotNetConfigPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetConfigPath.ps1 new file mode 100644 index 0000000..6dc90f5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetConfigPath.ps1 @@ -0,0 +1,16 @@ +function Get-DotNetConfigPath { +<# +.SYNOPSIS + Returns the path to the 64-bit machine.config +#> + param( + [bool]$Use64Bit = $true + ) + + if ($Use64Bit) { + return "C:\Windows\Microsoft.Net\Framework64\v4.0.30319\Config\machine.config" + } + + return "C:\Windows\Microsoft.Net\Framework\v4.0.30319\Config\machine.config" +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.ps1 new file mode 100644 index 0000000..751df22 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.ps1 @@ -0,0 +1,33 @@ +function Get-DotNetVersion { +<# +.SYNOPSIS + Returns the Registry Key Value and Friendly Version of the .NET Framework Which is Installed +#> + + [CmdletBinding()] + Param() + + $logLead = Get-LogLeadName + + Write-Host "$logLead : Checking Registry for Version Key" + $registryPath = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" + $netValueFromRegistry = Get-ItemProperty -Path $registryPath + + if ($null -eq $netValueFromRegistry -or $null -eq $netValueFromRegistry.Release) { + Write-Warning "$logLead : Unable to read the registry key from path $registryPath" + return + } + + Write-Verbose "$logLead : Release subkey value read as $($netValueFromRegistry.Release)" + + $friendlyVersion = $Global:DotNetVersionTranslation | Where-Object { $_.Key -match $netValueFromRegistry.Release } | Select-Object -First 1 -ErrorAction SilentlyContinue + + if ($null -eq $friendlyVersion -or $null -eq $friendlyVersion.FriendlyVersion) { + Write-Warning "$logLead : Unable to find a .NET version matching registry value $netValueFromRegistry.Release" + return + } + + Write-Host "$logLead : FriendlyVersion Identified as $($friendlyVersion.FriendlyVersion)" + + return $friendlyVersion +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.tests.ps1 new file mode 100644 index 0000000..6a78898 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-DotNetVersion.tests.ps1 @@ -0,0 +1,40 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-DotNetVersion + +Describe "Get-DotNetVersion" { + + It "Returns the .NET Friendly Name and Registry Key Value" { + + Mock -ModuleName $moduleForMock Get-ItemProperty { return New-Object PSObject -Property @{ Release="379893" } } + $testResult = Get-DotNetVersion -Verbose + $testResult.FriendlyVersion | Should Be "4.5.2" + $testResult.Key | Should Be "379893" + } + + It "Writes a Warning if the Key Can't be Found" { + + Mock -ModuleName Alkami.DevOps.Operations Get-ItemProperty { return $null } + + { + ( Get-DotNetVersion 3>&1 ) -match "Unable to read the registry key" + } | Should Be $true + } + + It "Writes a Warning if a FriendlyName Match Can't be Found" { + + Mock -ModuleName Alkami.DevOps.Operations Get-ItemProperty { return New-Object PSObject -Property @{ Release="99999" } } + + { + ( Get-DotNetVersion 3>&1 ) -match "Unable to find a .NET version" + } | Should Be $true + } +} + +#endregion Get-DotNetVersion \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-EnvironmentVariable.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-EnvironmentVariable.ps1 new file mode 100644 index 0000000..bbacff5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-EnvironmentVariable.ps1 @@ -0,0 +1,67 @@ +function Get-EnvironmentVariable { +<# +.SYNOPSIS + Get an environment variable. Will check the supplied store, or if no store supplied, checks all stores in order of process, user, machine. + +.PARAMETER Name + Gets the environment variable with the specified name. + +.PARAMETER StoreName + Checks the specified store name for the specified name. + If not present, checks all stores in order of process, user, machine. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("Key")] + [string]$Name, + [Parameter(Mandatory = $false)] + [Alias("Store")] + [Alias("Location")] + [ValidateSet("Process","User","Machine","Any")] + [string]$StoreName = "Any" + ) + + $logLead = Get-LogLeadName + + Write-Verbose "$logLead : Get environment variable [$Name] from $StoreName store" + + # Can't combine the three below like we do in Set- and Remove- so we can do the Any fallback + + if (($StoreName -eq "Any") -or ($StoreName -eq "Process")) { + $var = [System.Environment]::GetEnvironmentVariable($Name, [system.EnvironmentVariableTarget]::Process) + if ($null -ne $var) { + Write-Verbose "$logLead : Found EnvironmentVariable in the Process store" + return $var + } + } + + if (($StoreName -eq "Any") -or ($StoreName -eq "User")) { + $var = [System.Environment]::GetEnvironmentVariable($Name, [system.EnvironmentVariableTarget]::User) + if ($null -ne $var) { + Write-Verbose "$logLead : Found EnvironmentVariable in the User store" + return $var + } + } + + if (($StoreName -eq "Any") -or ($StoreName -eq "Machine")) { + $var = [System.Environment]::GetEnvironmentVariable($Name, [system.EnvironmentVariableTarget]::Machine) + if ($null -ne $var) { + Write-Verbose "$logLead : Found EnvironmentVariable in the Machine store" + return $var + } + } + + # We should not support dotted notation but we do because why not + # Typically we expect environment variables to use underscores. We should converge on that. + if ($Name.IndexOf('.') -gt -1) { + $underscoredName = $Name.Replace('.', '_') + Write-Verbose "$logLead : Value was not present dotted, will check with underscores for [$underscoredName]" + + return Get-EnvironmentVariable -Name $underscoredName -StoreName $StoreName + } + + Write-Verbose "$logLead : Could not find the key [$Name] in $StoreName store, returning `$null" + return $null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.ps1 new file mode 100644 index 0000000..ac7aae1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.ps1 @@ -0,0 +1,256 @@ +function Get-FileEncoding { +<# +.SYNOPSIS + Get a basic file encoding + +.DESCRIPTION + Get the file encoding in the most basic format + +.PARAMETER Path + The path to check. Can be a folder if you need all the files internally. + +.PARAMETER MaxFileLength + This is the maximum file size to try testing the whole file for when it might be Unicode or ASCII + +.PARAMETER TestForUTF7 + UTF7 is a very unusual format to look for. Use sparingly. +#> + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + $Path, + [Parameter(Mandatory=$false)] + [long]$MaxFileLength = 512000, + [Parameter(Mandatory=$false)] + [switch]$TestForUTF7 + ) + + $logLead = (Get-LogLeadName) + + $Path = (Resolve-Path $Path) + + Write-Verbose "$logLead : Checking $Path for file encoding" + $returns = @() + + $item = (Get-Item -Path $Path) + if ($item.PSIsContainer) { + $children = (Get-ChildItem -Path $Path) + foreach($child in $children) { + $returns += (Get-FileEncoding $child.FullName) + } + } else { + $bytes = [byte[]](Get-Content $Path -Encoding Byte -ReadCount 4 -TotalCount 4) + + $encoding = $null + $description = $null + + if (!(Test-IsCollectionNullOrEmpty $bytes)) { + # Run the first four bytes (returned above) through this formatting string and check the file contents + switch -regex ('{0:x2}{1:x2}{2:x2}{3:x2}' -f $bytes[0],$bytes[1],$bytes[2],$bytes[3]) { + '^efbbbf' { $encoding = [System.Text.Encoding]::UTF8; $description = 'UTF-8 encoded Unicode byte order mark, commonly seen in text files.'; break; } + '^fffe0000' { $encoding = [System.Text.Encoding]::UTF32; $description = 'UTF-8 encoded Unicode byte order mark little-endian 32-bit'; break; } + '^fffe' { $encoding = [System.Text.Encoding]::Unicode; $description = 'UTF-8 encoded Unicode byte order mark little-endian 16-bit'; break; } + '^feff' { $encoding = [System.Text.Encoding]::BigEndianUnicode; $description = 'UTF-8 encoded Unicode byte order mark (big-endian)'; break; } + '^0000feff' { $encoding = [System.Text.Encoding]::UTF32; $description = 'UTF-32 encoded Unicode byte order mark'; break; } + + # A lot of other non-text files that we may be curious what it is. This is a partial list. A lot of other file magic strings exist out there. + '^0000000c' { $encoding = $null; $description = 'JPEG 2000 graphic file'; break; } + '^00000018' { $encoding = $null; $description = 'Mpeg 4 video file'; break; } + '^00000100' { $encoding = $null; $description = 'Computer icon encoded in ICO file format'; break; } + '^000001b3' { $encoding = $null; $description = 'MPEG-1 video and MPEG-2 video (MPEG-1 Part 2 and MPEG-2 Part 2)'; break; } + '^000001ba' { $encoding = $null; $description = 'MPEG Program Stream (MPEG-1 Part 1 (essentially identical) and MPEG-2 Part 1)'; break; } + '^00010000' { $encoding = $null; $description = 'Palm Desktop Data File (Access format)'; break; } + '^00014244' { $encoding = $null; $description = 'Palm Desktop To Do Archive'; break; } + '^00014454' { $encoding = $null; $description = 'Palm Desktop Calendar Archive'; break; } + '^0061736d' { $encoding = $null; $description = 'WebAssembly binary format'; break; } + '^04224d18' { $encoding = $null; $description = 'LZ4 Frame Format'; break; } + '^05070000' { $encoding = $null; $description = 'AppleWorks 5 document'; break; } + '^0607e100' { $encoding = $null; $description = 'AppleWorks 6 document'; break; } + '^0a0d0d0a' { $encoding = $null; $description = 'PCAP Next Generation Dump File Format'; break; } + '^1a45dfa3' { $encoding = $null; $description = 'Matroska media container, including WebM'; break; } + '^1b4c7561' { $encoding = $null; $description = 'Lua bytecode'; break; } + '^1f8b' { $encoding = $null; $description = 'GZIP compressed file'; break; } + '^1f9d' { $encoding = $null; $description = 'tar zip'; break; } + '^1fa0' { $encoding = $null; $description = 'tar zip'; break; } + '^20020162' { $encoding = $null; $description = 'Tableau Datasource'; break; } + '^213c6172' { $encoding = $null; $description = 'linux deb file'; break; } + '^2142444e' { $encoding = $null; $description = 'Outlook Post Office file'; break; } + '^2321' { $encoding = $null; $description = 'Script or data to be passed to the program following the shebang (#!)'; break; } + '^24534449' { $encoding = $null; $description = 'System Deployment Image, a disk image format used by Microsoft'; break; } + '^2521' { $encoding = $null; $description = 'PostScript File'; break; } + '^25504446' { $encoding = $null; $description = 'PDF Document'; break; } + '^27051956' { $encoding = $null; $description = 'U-Boot / uImage. Das U-Boot Universal Boot Loader.'; break; } + '^28b52ffd' { $encoding = $null; $description = 'Zstandard compressed file'; break; } + '^3026b275' { $encoding = $null; $description = 'Windows Video file or Windows Audio file'; break; } + '^3082' { $encoding = $null; $description = 'DER encoded X.509 certificate'; break; } + '^310a3030' { $encoding = $null; $description = 'SubRip File'; break; } + '^3412aa55' { $encoding = $null; $description = 'VPK file, used to store game data for some Source Engine games'; break; } + '^37480302' { $encoding = $null; $description = 'KDB file'; break; } + '^377abcaf' { $encoding = $null; $description = '7-Zip File Format'; break; } + '^38425053' { $encoding = $null; $description = 'Photoshop Graphics'; break; } + '^3a290a' { $encoding = $null; $description = 'Smile file'; break; } + '^3d202020' { $encoding = $null; $description = 'Flexible Image Transport System (FITS)'; break; } + '^3f5f0300' { $encoding = $null; $description = 'Help file'; break; } + '^41474433' { $encoding = $null; $description = 'FreeHand 8 document'; break; } + '^41542654' { $encoding = $null; $description = 'DjVu document'; break; } + '^424d' { $encoding = $null; $description = 'Bitmap graphic'; break; } + '^425047fb' { $encoding = $null; $description = 'Better Portable Graphics format'; break; } + '^425a68' { $encoding = $null; $description = 'Compressed file using Bzip2 algorithm'; break; } + '^435753' { $encoding = $null; $description = 'flash .swf'; break; } + '^43723234' { $encoding = $null; $description = 'Google Chrome extension or packaged app'; break; } + '^44434d01' { $encoding = $null; $description = 'Windows Update Binary Delta Compression'; break; } + '^454d5533' { $encoding = $null; $description = 'Emulator III synth samples'; break; } + '^454d5832' { $encoding = $null; $description = 'Emulator Emaxsynth samples'; break; } + '^45520200' { $encoding = $null; $description = 'Roxio Toast disc image file, also some .dmg-files begin with same bytes'; break; } + '^464c4946' { $encoding = $null; $description = 'Free Lossless Image Format'; break; } + '^464c56' { $encoding = $null; $description = 'Flash Video'; break; } + '^465753' { $encoding = $null; $description = 'Flash Shockwave'; break; } + '^47494638' { $encoding = $null; $description = 'GIF graphic file'; break; } + '^47' { $encoding = $null; $description = 'MPEG Transit Stream'; break; } + '^494433' { $encoding = $null; $description = 'MP3 file with ID3 identity tag'; break; } + '^4949' { $encoding = $null; $description = 'TIF graphic file'; break; } + '^494e4458' { $encoding = $null; $description = 'Index file to a file or tape containing a backup done with AmiBack on an Amiga.'; break; } + '^49545346' { $encoding = $null; $description = 'MS Windows HtmlHelp Data'; break; } + '^4a6f7921' { $encoding = $null; $description = 'Preferred Executable Format'; break; } + '^4b444d56' { $encoding = $null; $description = 'VMWare Disk file'; break; } + '^4b444d' { $encoding = $null; $description = 'VMDK files'; break; } + '^4c01' { $encoding = $null; $description = 'Object Code File'; break; } + '^4c5a4950' { $encoding = $null; $description = 'lzip compressed file'; break; } + '^4d4c5649' { $encoding = $null; $description = 'Magic Lantern Video file'; break; } + '^4d4d002a' { $encoding = $null; $description = 'Tagged Image File Format (TIFF) (big-endian format)'; break; } + '^4d534346' { $encoding = $null; $description = 'CAB Installer file'; break; } + '^4d546864' { $encoding = $null; $description = 'MIDI sound file'; break; } + '^4d5a' { $encoding = $null; $description = 'Windows MZ/PE/NE or SYS (driver) file'; break; } + '^4d696372' { $encoding = $null; $description = 'Microsoft Build System File (pdb, etc)'; break; } + '^4e45531a' { $encoding = $null; $description = 'Nintendo Entertainment System ROM file'; break; } + '^4f4152' { $encoding = $null; $description = 'OAR file archive format, where ?? is the format version.'; break; } + '^4f5243' { $encoding = $null; $description = 'Apache ORC (Optimized Row Columnar) file format'; break; } + '^4f626a01' { $encoding = $null; $description = 'Apache Avro binary file format'; break; } + '^4f676753' { $encoding = $null; $description = 'Ogg, an open source media container format'; break; } + '^50415231' { $encoding = $null; $description = 'Apache Parquet columnar file format'; break; } + '^504b0304' { $encoding = $null; $description = 'ZIP file (or masquerading file, such as Nuget, Choco, EPUB, JAR, ODF, OOXML, Office document)'; break; } + '^504b0506' { $encoding = $null; $description = 'zip file (empty archive)'; break; } + '^504b0708' { $encoding = $null; $description = 'zip file (spanned archive)'; break; } + '^504d4f43' { $encoding = $null; $description = 'Windows Files And Settings Transfer Repository'; break; } + '^52494646' { $encoding = $null; $description = 'AVI video file or WAV audio file'; break; } + '^524e4301' { $encoding = $null; $description = 'Compressed file using Rob Northen Compression (version 1 and 2) algorithm'; break; } + '^524e4302' { $encoding = $null; $description = 'Compressed file using Rob Northen Compression (version 1 and 2) algorithm'; break; } + '^5253564b' { $encoding = $null; $description = 'QuickZip rs compressed archive'; break; } + '^52617221' { $encoding = $null; $description = 'RAR file'; break; } + '^52656365' { $encoding = $null; $description = 'Email Message var5'; break; } + '^53445058' { $encoding = $null; $description = 'SMPTE DPX image (big-endian format)'; break; } + '^53455136' { $encoding = $null; $description = 'RCFile columnar file format'; break; } + '^53494d50' { $encoding = $null; $description = 'Flexible Image Transport System (FITS)'; break; } + '^53503031' { $encoding = $null; $description = 'Amazon Kindle Update Package'; break; } + '^53514c69' { $encoding = $null; $description = 'SQLite Database'; break; } + '^535a4444' { $encoding = $null; $description = 'Microsoft compressed file. File can be decompressed using Extract.exe/Expand.exe distributed with earlier versions of Windows.'; break; } + '^5374616e' { $encoding = $null; $description = 'Microsoft Database'; break; } + '^54415045' { $encoding = $null; $description = 'Microsoft Tape Format'; break; } + '^54444546' { $encoding = $null; $description = 'Telegram Desktop Encrypted File'; break; } + '^54444624' { $encoding = $null; $description = 'Telegram Desktop File'; break; } + '^5555aaaa' { $encoding = $null; $description = 'PhotoCap Vector'; break; } + '^58464952' { $encoding = $null; $description = 'Adobe Shockwave'; break; } + '^58504453' { $encoding = $null; $description = 'SMPTE DPX image (little-endian format)'; break; } + '^5a4d' { $encoding = $null; $description = 'DOS ZM executable file format and its descendants (rare)'; break; } + '^5b5a6f6e' { $encoding = $null; $description = 'Microsoft Zone Identifier for URL Security Zones'; break; } + '^626f6f6b' { $encoding = $null; $description = 'macOS file Alias (Symbolic link)'; break; } + '^62767832' { $encoding = $null; $description = 'LZFSE - Lempel-Ziv style data compression algorithm using Finite State Entropy coding. OSS by Apple.'; break; } + '^6465780a' { $encoding = $null; $description = 'Dalvik Executable'; break; } + '^65877856' { $encoding = $null; $description = 'PhotoCap Object Templates'; break; } + '^664c6143' { $encoding = $null; $description = 'Free Lossless Audio Codec'; break; } + '^6d6f6f76' { $encoding = $null; $description = 'MOV video file'; break; } + '^746f7833' { $encoding = $null; $description = 'Open source portable voxel file'; break; } + '^75737461' { $encoding = $null; $description = 'Tar file'; break; } + '^762f3101' { $encoding = $null; $description = 'OpenEXR image'; break; } + '^774f4632' { $encoding = $null; $description = 'WOFF File Format 2.0'; break; } + '^774f4646' { $encoding = $null; $description = 'WOFF File Format 1.0'; break; } + '^7801' { $encoding = $null; $description = 'zlib - No Compression (no preset dictionary)'; break; } + '^7801730d' { $encoding = $null; $description = 'Apple Disk Image file'; break; } + '^7820' { $encoding = $null; $description = 'zlib - No Compression (with preset dictionary)'; break; } + '^785634' { $encoding = $null; $description = 'PhotoCap Template'; break; } + '^785e' { $encoding = $null; $description = 'zlib - Best speed (no preset dictionary)'; break; } + '^78617221' { $encoding = $null; $description = 'eXtensible ARchive format'; break; } + '^787d' { $encoding = $null; $description = 'zlib - Best speed (with preset dictionary)'; break; } + '^789c' { $encoding = $null; $description = 'zlib - Default Compression (no preset dictionary)'; break; } + '^78bb' { $encoding = $null; $description = 'zlib - Default Compression (with preset dictionary)'; break; } + '^78da' { $encoding = $null; $description = 'zlib - Best Compression (no preset dictionary)'; break; } + '^78f9' { $encoding = $null; $description = 'zlib - Best Compression (with preset dictionary)'; break; } + '^7b5c7274' { $encoding = $null; $description = 'Rich Text Format'; break; } + '^7f454c46' { $encoding = $null; $description = 'Executable and Linkable Format'; break; } + '^802a5fd7' { $encoding = $null; $description = 'Kodak Cineon image'; break; } + '^85' { $encoding = $null; $description = 'PGP file'; break; } + '^89504e47' { $encoding = $null; $description = 'PNG graphic file'; break; } + '^8b455202' { $encoding = $null; $description = 'Roxio Toast disc image file, also some .dmg-files begin with same bytes'; break; } + '^a1b2c3d4' { $encoding = $null; $description = 'Libpcap File Format (big-endian)'; break; } + '^bebafeca' { $encoding = $null; $description = 'Palm Desktop Calendar Archive'; break; } + '^c9' { $encoding = $null; $description = 'CP/M 3 and higher with overlays'; break; } + '^cafebabe' { $encoding = $null; $description = 'Java class file, Mach-O Fat Binary'; break; } + '^cefaedfe' { $encoding = $null; $description = 'Mach-O binary (reverse byte ordering scheme, 32-bit)'; break; } + '^cf8401' { $encoding = $null; $description = 'Lepton compressed JPEG image'; break; } + '^cffaedfe' { $encoding = $null; $description = 'Mach-O binary (reverse byte ordering scheme, 64-bit)'; break; } + '^d0cf11e0' { $encoding = $null; $description = 'Office Document'; break; } + '^d4c3b2a1' { $encoding = $null; $description = 'Libpcap File Format (little-endian)'; break; } + '^d7cdc69a' { $encoding = $null; $description = 'Windows Meta File'; break; } + '^edabeedb' { $encoding = $null; $description = 'RedHat Package Manager (RPM) package'; break; } + '^fd377a58' { $encoding = $null; $description = 'XZ compression utility using LZMA2 compression'; break; } + '^feedfeed' { $encoding = $null; $description = 'JKS JavakeyStore'; break; } + '^ffd8' { $encoding = $null; $description = 'jpg'; break; } + '^fff2' { $encoding = $null; $description = 'MPEG-1 Layer 3 file'; break; } + '^fff3' { $encoding = $null; $description = 'MPEG-1 Layer 3 file'; break; } + '^fffb' { $encoding = $null; $description = 'MPEG-1 Layer 3 file'; break; } + } + + if ($null -eq $description) { + if ($item.Length -lt $MaxFileLength) { + $bytes = [byte[]](Get-Content $Path -Encoding Byte -Raw) + } + $isAscii = $true + $byteCounter = 0 + foreach($byte in $bytes) { + $byteCounter += 1 + $convertedByte = [byte]([char]$byte) + # If it is a tab (9), LF (10), CR (13) or alphanumeric+symbols (32-126), it's valid + $whitespaceChars = $convertedByte -eq 9 -or $convertedByte -eq 10 -or $convertedByte -eq 13 + if ($whitespaceChars) { + continue + } + + if ($convertedByte -gt 126 -or $convertedByte -lt 32) { + # Byte is outside the typical range of bytes for ASCII text + $isAscii = $false + $description = "File appears to contain non-ASCII characters (current byte value: $convertedByte at counter: $byteCounter)" + break + } + } + if ($isAscii) { + $utf7TestResult = $false + # You can't know if something is UTF7 without reading it entirely in and trying to parse as UTF7. + # UTF7 is literally "encode Unicode as ASCII" much as base64 encoding might + if ($TestForUTF7) { + Write-Host "$logLead : Testing for UTF7" + $asciiTextRaw = (Get-Content -Path $Path -Encoding ASCII -Raw) + $utf7TextRaw = (Get-Content -Path $Path -Encoding UTF7 -Raw) + + if ($asciiTextRaw -ne $utf7TextRaw) { + $encoding = [System.Text.Encoding]::UTF7 + $description = "File appears to be UTF7 encoded" + + $utf7TestResult = $true + } + } + + if (!$utf7TestResult) { + $encoding = [System.Text.Encoding]::ASCII + $description = "File appears to only contain ASCII characters" + } + } + } + } else { + $description = "Zero-byte file, can not determine encoding" + } + + $returns += @(@{File = $Path.Path; Encoding = $encoding; Description = $description; }) + } + + return $returns +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.tests.ps1 new file mode 100644 index 0000000..18c7eb7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FileEncoding.tests.ps1 @@ -0,0 +1,90 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-FileEncoding + +$asciiTestString = "The quick brown fox jumps over the lazy dog" +$nonasciiTestString = [string]::Join('',('z', 'a', [char]0x0306, [char]0x01FD, [char]0x03B2, [char]0xD8FF, [char]0xDCFF)) + +$asciiEncodingExpectedResult = ([System.Text.Encoding]::ASCII).ToString() +$utf8EncodingExpectedResult = ([System.Text.Encoding]::UTF8).ToString() +$uniEncodingExpectedResult = ([System.Text.Encoding]::Unicode).ToString() +$utf7EncodingExpectedResult = ([System.Text.Encoding]::UTF7).ToString() + +Describe "Get-FileEncoding" -Tags @("Integration") { + Mock -ModuleName $moduleForMock Get-LogLeadName { "UUT: Get-FileEncoding" } + Mock -ModuleName $moduleForMock Write-Host {} + + It "reads a known ASCII file as ASCII" { + $testDrivePath = "TestDrive:\ascii.txt" + Set-Content -Path $testDrivePath -Value $asciiTestString -Encoding ASCII + $result = Get-FileEncoding $testDrivePath + + $result.Encoding | Should -Be $asciiEncodingExpectedResult + } + + It "reads a known UTF8 file with ASCII text as UTF8" { + $testDrivePath = "TestDrive:\UTF8.txt" + Set-Content -Path $testDrivePath -Value $asciiTestString -Encoding UTF8 + $result = Get-FileEncoding $testDrivePath + + $result.Encoding | Should -Be $utf8EncodingExpectedResult + } + + It "reads a known UTF8 file with non-ASCII text as UTF8" { + $testDrivePath = "TestDrive:\UTF8.txt" + Set-Content -Path $testDrivePath -Value $nonasciiTestString -Encoding UTF8 + $result = Get-FileEncoding $testDrivePath + + $result.Encoding | Should -Be $utf8EncodingExpectedResult + } + + It "reads a known Unicode file with non-ASCII text as Unicode" { + $testDrivePath = "TestDrive:\Unicode.txt" + Set-Content -Path $testDrivePath -Value $nonasciiTestString -Encoding Unicode + $result = Get-FileEncoding $testDrivePath + + $result.Encoding | Should -Be $uniEncodingExpectedResult + } + + It "reads a known Unicode file with ASCII text as Unicode" { + $testDrivePath = "TestDrive:\Unicode.txt" + Set-Content -Path $testDrivePath -Value $asciiTestString -Encoding Unicode + $result = Get-FileEncoding $testDrivePath + + $result.Encoding | Should -Be $uniEncodingExpectedResult + } + + # Not running theascii scenario for UTF7 + # It's impossible to read a UTF7 file as UTF7 if it only contains ASCII. + # UTF7 is a variant of base64 or urlencode for Unicode chars to ASCII files + It "reads a known UTF7 file with non-ASCII text as UTF7" { + $testDrivePath = "TestDrive:\UTF7.txt" + Set-Content -Path $testDrivePath -Value $nonasciiTestString -Encoding UTF7 + $result = Get-FileEncoding $testDrivePath -TestForUTF7 + + $result.Encoding | Should -Be $utf7EncodingExpectedResult + } + + It "reads a known exe file as MZ/PE/NE" { + $testDrivePath = "c:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + + # This file "should" exist if we are testing on Windows. + # Seems like testing for powershell is an easy win :D ows trick. See what I did there? + # This is just to prove that testing for PE/NE/MZ files does the thing because .NET .dll files are also PE/NE/MZ + if (Test-Path $testDrivePath) { + # this is brittle because if someone updates the result from the actual function, this will break + $brittleTestStringResult = 'Windows MZ/PE/NE or SYS (driver) file' + $result = Get-FileEncoding $testDrivePath + + $result.Description | Should -Be $brittleTestStringResult + } + } +} + +#endregion Get-FileEncoding \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FilesNoSymlink.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FilesNoSymlink.ps1 new file mode 100644 index 0000000..2ce4252 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FilesNoSymlink.ps1 @@ -0,0 +1,23 @@ +function Get-FilesNoSymlink { +<# +.SYNOPSIS + Returns a recursive list of files without following symlinks. +#> + param( + $Path + ) + + $fc = new-object -com scripting.filesystemobject + $folder = $fc.getfolder($Path) + + foreach ($file in $folder.files) { $file | Select-Object -ExpandProperty Path } + + foreach ($subfolder in $folder.subfolders) + { + if ( (get-item $subfolder.path).Attributes.ToString() -notmatch "ReparsePoint") + { + Get-FilesNoSymlink($subfolder.path) + } + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.Tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.Tests.ps1 new file mode 100644 index 0000000..a0092e2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.Tests.ps1 @@ -0,0 +1,58 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-FilteredStringArray" { + $itemArrayBaseline = @("match1","match2","match3","matchdupe","matchdupe","filler1","filler2") + + $zeroMatchFilter = @("nothingmatchesthis") + $zeroMatchFilterWithSpecialCharacters = @("filler.",".filler","filler*","*filler","filler1.","filler1*") + $oneMatchFilter = @("match1") + $oneMatchFilterExpectedResult = @("match1") + + $twoMatchFilter = @("match1","match2") + $twoMatchFilterExpectedResult = @("match2","match1") + + $oneMatchFilterWithDupeFilter = @("match3","match3") + $oneMatchFilterWithDupeFilterExpectedResult = @("match3") + + $dupeMatchFilter = @("matchdupe") + $dupeMatchFilterExpectedResult = @("matchdupe") + + $dupeMatchFilterWithDupeFilter = @("matchdupe","matchdupe") + $dupeMatchFilterWithDupeFilterExpectedResult = @("matchdupe") + + It "returns empty array when nothing matches" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $zeroMatchFilter + $retVal | Should -BeNullOrEmpty + } + It "returns empty array when nothing matches including '.' and '*'" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $zeroMatchFilterWithSpecialCharacters + $retVal | Should -BeNullOrEmpty + } + It "returns array with one matching element when one filter matches" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $oneMatchFilter + $retVal | Should -Be $oneMatchFilterExpectedResult + } + It "returns array with two matching elements when two filters match" { + #Compare-Object allows us to compare two arrays and not care about the order. + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $twoMatchFilter + Compare-Object $retVal $twoMatchFilterExpectedResult | Should -Be $null + } + It "returns array with one matching element when duplicate filter matches" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $oneMatchFilterWithDupeFilter + $retVal | Should -Be $oneMatchFilterWithDupeFilterExpectedResult + } + It "returns array with one matching element when duplicate ITEM matches singular FILTER" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $dupeMatchFilter + $retVal | Should -Be $dupeMatchFilterExpectedResult + } + It "returns array with one matching element when duplicate ITEM matches duplicate FILTER" { + $retVal = Get-FilteredStringArray -InputArray $itemArrayBaseline -FilterArray $dupeMatchFilterWithDupeFilter + $retVal | Should -Be $dupeMatchFilterWithDupeFilterExpectedResult + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.ps1 new file mode 100644 index 0000000..0867a7f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FilteredStringArray.ps1 @@ -0,0 +1,33 @@ +function Get-FilteredStringArray { +<# +.SYNOPSIS + Filter an array of strings by another array of strings + +.DESCRIPTION + Each item in that is present in is added to the return value, which is a [string[]] + +.PARAMETER InputArray + [string[]] One or more items to be filtered + +.PARAMETER FilterArray + [string[]] One or more items used to filter +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string[]]$InputArray, + [Parameter(Mandatory = $true)] + [string[]]$FilterArray +) + $filteredArray = @(); + foreach ($item in $InputArray) { + # `-in` is the flipside of `-contains`, verifying if an item exists as an element in an array. + # See "Containment" - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators?view=powershell-6 + + # We are using `-in` because it reads, left-to-right, easier + if ($item -in $FilterArray -and $item -notin $filteredArray) { + $filteredArray += $item + } + } + return $filteredArray +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.ps1 new file mode 100644 index 0000000..345c345 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.ps1 @@ -0,0 +1,58 @@ +function Get-FolderSizeMb { + <# +.SYNOPSIS + Retrieves the size in MB of the specified folder. + +.PARAMETER Path + The path to evaluate. + +.PARAMETER ComputerName + The name of the server where the operation should be performed. If omitted, defaults to the current host. + +.OUTPUTS + The size of the specified folder in MB. + +.EXAMPLE + Get-FolderSizeMb -Path 'C:\Temp' + +129.49 + +.EXAMPLE + Get-FolderSizeMb -Path 'C:\This\Path\Does\Not\Exist' + +0 +#> + [CmdLetBinding()] + [OutputType([decimal])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [Parameter(Mandatory = $false)] + [string] $ComputerName = $null + ) + + $scriptBlock = { + param($p) + + if ( Test-Path $p -PathType Container ) { + + return [System.Math]::Round( ((Get-ChildItem -Path $p -Recurse -ErrorAction SilentlyContinue -Force) | Measure-Object -Property Length -Sum).Sum / 1Mb, 2) + + } else { + + return 0 + } + } + + # Determine if we are executing remotely. + $splatParams = @{} + if ( -not (Test-StringIsNullOrWhitespace -Value $ComputerName) -and -not (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName )) { + + $splatParams['ComputerName'] = "$ComputerName" + } + + $result = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $Path @splatParams + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.tests.ps1 new file mode 100644 index 0000000..ae1b941 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FolderSizeMb.tests.ps1 @@ -0,0 +1,113 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = '' + +Describe 'Get-FolderSizeMb' { + + $testSize = 1024 * 1024 + $testPath = 'TestDrive:\Test' + $testServer = 'server1.test.local' + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ChildItem -ModuleName $moduleForMock -MockWith { return @(@{Length = $testSize }) } + Mock -CommandName Measure-Object -ModuleName $moduleForMock -MockWith { return @{Sum = $testSize } } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Invoke-Command -ModuleName $moduleForMock -MockWith { + return $ScriptBlock.Invoke(@($testPath)) + } + + Context 'Input Validation' { + + It 'Throws if Path is null' { + + { Get-FolderSizeMb -Path $null } | Should -Throw + } + + It 'Throws if Path is empty' { + + { Get-FolderSizeMb -Path '' } | Should -Throw + } + } + + Context 'Logic' { + + It 'Performs command on localhost by default' { + + Get-FolderSizeMb -Path $testPath | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-Path -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Measure-Object -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + } + + It 'Performs command on localhost if ComputerName matches current server' { + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $false } + + Get-FolderSizeMb -Path $testPath -ComputerName $testServer | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { ( -not $PSBoundParameters.ContainsKey('ComputerName')) } + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Performs command on remote server if ComputerName does not match current server' { + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $false } + + $testServer2 = 'server2.test.local' + + Get-FolderSizeMb -Path $testPath -ComputerName $testServer2 | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -Times 1 -Exactly -Scope It ` + -ParameterFilter { $stringToCheck -eq $testServer2 } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It ` + -ParameterFilter { $ComputerName -eq $testServer2 } + + Mock -CommandName Test-StringIsNullOrWhitespace -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Returns zero if path not found' { + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $false } + + $result = Get-FolderSizeMb -Path $testPath + $result | Should -BeExactly 0 + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-Path -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Measure-Object -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It + + Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true } + } + + It 'Returns size in MB if path is found' { + + $result = Get-FolderSizeMb -Path $testPath + $result | Should -BeExactly ($testSize / 1Mb) + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-Path -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Measure-Object -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 1 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-FullyQualifiedServerName.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-FullyQualifiedServerName.ps1 new file mode 100644 index 0000000..4ff5613 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-FullyQualifiedServerName.ps1 @@ -0,0 +1,21 @@ +function Get-FullyQualifiedServerName { +<# +.SYNOPSIS + Returns the fully qualified server name +#> + param ( + + ) + + $globalIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties(); + + # If no domain is present, return just the hostname. If there is a domain, . append it after the hostname + if ($globalIPProperties.DomainName.length -gt 0) { + $qualifiedServerName = "{0}.{1}" -f $globalIPProperties.HostName, $globalIPProperties.DomainName; + } + else { + $qualifiedServerName = $globalIPProperties.HostName; + } + + return $qualifiedServerName +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.ps1 new file mode 100644 index 0000000..e226f8a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.ps1 @@ -0,0 +1,22 @@ +function Get-HostsFileContent { +<# +.SYNOPSIS + Reads the Contents of the Hosts File +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $false)] + [string]$hostsPath = "$env:windir\System32\Drivers\etc\hosts" + ) + + $logLead = (Get-LogLeadName); + + if (!(Test-Path $hostsPath)) { + Write-Warning ("$logLead : Could not read hosts file from {0}" -f $hostsPath) + return + } + + Write-Verbose ("$logLead : Reading Hosts file from {0}" -f $hostsPath) + return [System.IO.File]::ReadAllLines($hostsPath) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.tests.ps1 new file mode 100644 index 0000000..b77d856 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-HostsFileContent.tests.ps1 @@ -0,0 +1,33 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-HostsFileContent + +Describe "Get-HostsFileContent" { + + Context "When a bad path is supplied to Get-HostsFileContent" { + + It "Writes a Warning" { + + { + $tempPath = [System.IO.Path]::GetTempFileName() + ((Get-HostsFileContent $tempPath) 3>&1) -match "Could not read hosts file from" + } | Should Be $true + } + } + + Context "When default parameters are used" { + + It "Returns Some Content" { + + Get-HostsFileContent | Should Not Be $null + } + } +} + +#endregion Get-HostsFileContent \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-IPAddressesForName.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-IPAddressesForName.ps1 new file mode 100644 index 0000000..2527bc7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-IPAddressesForName.ps1 @@ -0,0 +1,21 @@ +Function Get-IPAddressesForName { +<# +.SYNOPSIS + Get the IP addresses for a hostname, or return the IP address passed in +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $true)] + [string]$hostname + ) + + if (!$hostname) { + throw 'must supply a value' + } + if (!!($hostname -As [IPAddress])) { + $hostname; + } else { + (Resolve-DnsName -Type A $hostname).IPAddress; + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.ps1 new file mode 100644 index 0000000..eb1c5a4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.ps1 @@ -0,0 +1,15 @@ +function Get-ImdsBaseUri { + <# + .SYNOPSIS + Gets the Instance MetaData Service (IMDS) URI + + .NOTES + Does not return a final trailing slash '/' at the end of the URI + https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html + #> + [CmdletBinding()] + [OutputType([System.String])] + param () + + return "http://169.254.169.254/latest" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.tests.ps1 new file mode 100644 index 0000000..f319409 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsBaseUri.tests.ps1 @@ -0,0 +1,21 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +# $moduleForMock = "" + +Describe "Get-ImdsBaseUri" { + + Context "Data Evaluation" { + + # This test makes sure someone doesn't change the IMDS URI nor add a trailing '/' + It "Should Return IMDS Endpoint" { + + $compareUri = "http://169.254.169.254/latest" + $returnUri = Get-ImdsBaseUri + $returnUri | Should -Be $compareUri + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.ps1 new file mode 100644 index 0000000..2f3e3a7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.ps1 @@ -0,0 +1,66 @@ +function Get-ImdsV2Token { + <# + .SYNOPSIS + This gets the token needed for IMDS V2 validation + + .DESCRIPTION + For IMDS V2 calls, a token must be retrieved that has a short lifespan. That token is then + used in the header for subsequent calls to the IMDS service. + + This function takes care of the lifecycle of the token. Callers need not worry about + caching or storing the token or when/how to refresh it. + + The token is an instance-specific key. The token is not valid on other EC2 instances + and will be rejected if you attempt to use it outside of the instance on which it was generated. + + .PARAMETER InvalidateCache + When set, this will bust the cache for the token currently set and cause this function + to generate a new token and set it in the cache. + + .PARAMETER TTL + How long the token should live, in seconds. This is set default at 5 minutes (300 seconds). The service + minimum is 1 second and maximum of 6 hours (21,600 seconds). + + .EXAMPLE + $token = Get-ImdsV2Token + + .NOTES + https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html + #> + [CmdletBinding()] + [OutputType([System.String])] + Param ( + [Parameter(Mandatory = $false)] + [switch]$InvalidateCache, + + [Parameter(Mandatory = $false)] + [int]$TTL = 300 + ) + + $logLead = (Get-LogLeadName) + + # Test bounds of $TTL. + if(($TTL -lt 1) -or ($TTL -gt 21600)) { + throw "TTL is out of bounds. Must be between 1 and 21600." + } + + # Get the token from cache. + $token = $Global:AlkamiImdsSessionToken + + # Token is not null and $InvalidateCache is not set, return cached token. + if(!$InvalidateCache -and ($null -ne $token) ) { + Write-Verbose "$logLead token is not null and InvalidateCache is false. Returning cached token." + return $token + } + + $uri = (Get-ImdsBaseUri) + $endpoint = ("{0}/api/token" -f $uri) + + Write-Verbose "$logLead getting new token with TTL of $TTL seconds." + $token = (Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token-ttl-seconds" = $TTL} -Method PUT -Uri $endpoint) + + # Cache token. + $Global:AlkamiImdsSessionToken = $token + + return $token +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.tests.ps1 new file mode 100644 index 0000000..decdff2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ImdsV2Token.tests.ps1 @@ -0,0 +1,56 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ImdsV2Token" { + + Context "Parameter Validation" { + + It "Too Low TTL" { + + { Get-ImdsV2Token -TTL 0 } | Should Throw + } + + It "Too High TTL" { + + { Get-ImdsV2Token -TTL 21601 } | Should Throw + } + } + + Context "Cache Handling" { + + $Global:AlkamiImdsSessionToken = $null + Mock -ModuleName $moduleForMock Invoke-RestMethod { return "1234567890" } + + It "Sets Token In Cache" { + + $token = Get-ImdsV2Token + + Assert-MockCalled -ModuleName $moduleForMock Invoke-RestMethod -Times 1 -Exactly -Scope It + $token | Should -Be $Global:AlkamiImdsSessionToken + } + + It "Retrieves Token From Cache" { + + $token = Get-ImdsV2Token + + Assert-MockCalled -ModuleName $moduleForMock Invoke-RestMethod -Times 0 -Exactly -Scope It + $token | Should -Be $Global:AlkamiImdsSessionToken + } + + It "Breaks The Cache" { + + $Global:AlkamiImdsSessionToken = "1234567890" + Mock -ModuleName $moduleForMock Invoke-RestMethod { return "0987654321" } + + $token = Get-ImdsV2Token -InvalidateCache + + Assert-MockCalled -ModuleName $moduleForMock Invoke-RestMethod -Times 1 -Exactly -Scope It + $token | Should -Be $Global:AlkamiImdsSessionToken + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-InstanceMetadata.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceMetadata.ps1 new file mode 100644 index 0000000..8f27247 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceMetadata.ps1 @@ -0,0 +1,81 @@ +function Get-InstanceMetadata { + <# + .SYNOPSIS + This function wraps the web-request to get the Instance Metadata (IMDS) from an EC2 instance. + + .DESCRIPTION + Defaults to using V2 of the service, which requires a token. This function takes care of the token + generation and use. + + There's also the option to use V1 of the service with the switch $UseImdsV1, which requires no token. + + .PARAMETER Endpoint + The endpoint in the IMDS servicec to fetch. Ex. '/meta-data/hostname' + + .PARAMETER UseImdsV1 + Switch to change to the IMDS V1 request instead of V2. + + .EXAMPLE + $result = Get-InstanceMetadata -Endpoint "/meta-data/hostname" + $result = Get-InstanceMetadata -Endpoint "/meta-data/hostname" -UseImdsV1 + + .NOTES + https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html + #> + [CmdletBinding()] + [OutputType([System.String])] + Param ( + [Parameter(Mandatory = $true)] + [string]$Endpoint, + + [Parameter(Mandatory = $false)] + [switch]$UseImdsV1 + ) + $logLead = (Get-LogLeadName) + + # Script block to be used when Imds V2 is selected. This is default unless switch $UseImdsV1 is specified. + $V2Script = { + param ($sbImdsUri, $sbEndpoint, $sbImdsV2Token) + + $endpoint = ("{0}/{1}" -f $sbImdsUri, $sbEndpoint) + + try { + $metadata = (Invoke-WebRequest -Headers @{"X-aws-ec2-metadata-token" = $sbImdsV2Token} -Method GET -UseBasicParsing -TimeoutSec 5 -Uri $endpoint) + } catch [System.Net.WebException] { + # Only on 401 + $unauthorizedStatusCode = [System.Net.HttpStatusCode]::Unauthorized + $response = $_.Exception.Response + + if($response.StatusCode -eq $unauthorizedStatusCode) { + Write-Verbose "401 Unauthorized caught. Trying again with new token." + $newToken = Get-ImdsV2Token -InvalidateCache + $metadata = (Invoke-WebRequest -Headers @{"X-aws-ec2-metadata-token" = $newToken} -Method GET -UseBasicParsing -TimeoutSec 5 -Uri $endpoint) + } + } + return $metadata + } + + # Script block to be used when ImdsV1 is specified. + $V1Script = { + param($sbImdsUri, $sbEndpoint) + + $endpoint = ("{0}/{1}" -f $sbImdsUri, $sbEndpoint) + $metadata = (Invoke-WebRequest -Method GET -UseBasicParsing -TimeoutSec 5 -Uri $endpoint) + return $metadata + } + + # Set up variables to be passed into the script blocks. + $imdsUri = Get-ImdsBaseUri + + # Switch between the two commands based on the $UseImdsV1 switch param. + if($UseImdsV1) { + Write-Verbose "$logLead Imds V1 specified, trying command with V1 URI." + $result = (Invoke-CommandWithRetry -Arguments ($imdsUri, $Endpoint) -MaxRetries 3 -Exponential -ScriptBlock $V1Script) + } else { + Write-Verbose "$logLead Imds V2 specified, trying command with V2 URI." + $token = Get-ImdsV2Token + $result = (Invoke-CommandWithRetry -Arguments ($imdsUri, $Endpoint, $token) -MaxRetries 3 -Exponential -ScriptBlock $V2Script) + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.Tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.Tests.ps1 new file mode 100644 index 0000000..08f52be --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.Tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-InstanceTags" { + + Context "When Called For a Server" { + + Mock -ModuleName $moduleForMock Invoke-Command { + $tag = [PSCustomObject]@{ + PSComputerName = "MyServer.fh.local" + RunspaceId = "11111111-1111-1111-1111-111111111111" + Key = "alk:tagenv" + Value = "fakeEnv" + } + + return $tag + } + Mock -ModuleName $moduleForMock Write-Host {} + + It "Returns Tags for Specified Server" { + $serverName = "MyServer.fh.local" + + $tags = Get-InstanceTags $serverName + + Assert-MockCalled -ModuleName $moduleForMock Invoke-Command + $tags.PSComputerName | Should -Be $serverName + $tags.Key | Should -Not -Be $null + $tags.Value | Should -Not -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.ps1 new file mode 100644 index 0000000..43fa200 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-InstanceTags.ps1 @@ -0,0 +1,25 @@ +function Get-InstanceTags { + <# + .SYNOPSIS + Get tags from the specified server + .PARAMETER ServerToTest + Server to remote into to get the tags from. + #> + [CmdletBinding()] + param( + $ServerToTest + ) + + $logLead = (Get-LogLeadName); + + $scriptBlock = { + + $tags = Get-CurrentInstanceTags + return $tags + } + + Write-Host "$logLead : Attempting to get tags from $ServerToTest." + $tags = Invoke-Command -ScriptBlock $scriptBlock -ComputerName $ServerToTest + + return $tags +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-IpAddress.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-IpAddress.ps1 new file mode 100644 index 0000000..4930147 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-IpAddress.ps1 @@ -0,0 +1,24 @@ +function Get-IpAddress { +<# +.SYNOPSIS + Returns the IP address of the server. Returns the private 10.* IP, or otherwise the first IPv4 address. +#> + [CmdletBinding()] + Param() + + $ips = Get-NetIPAddress | Where-Object { ($_.AddressState -eq "preferred") -and ($_.AddressFamily -eq "IPv4") -and ($_.IPAddress -ne "127.0.0.1")}; + + $privateIP = $ips | Where-Object { $_.IPAddress -like "10.*"; } | Select-Object -First 1; + if($null -ne $privateIP) + { + return $privateIP.IPAddress; + } + + $ip = $ips | Select-Object -First 1; + if($null -ne $ip) + { + return $ip.IPAddress; + } + + return $null; +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-LogColor.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-LogColor.ps1 new file mode 100644 index 0000000..8d627f6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-LogColor.ps1 @@ -0,0 +1,39 @@ +function Get-LogColor { + <# + .SYNOPSIS + Takes a seed (function name) then consistently returns a foreground Color + .EXAMPLE + $logColor = Get-logColor $logLead + .PARAMETER Seed + Seed input should be the output of Get-LogLeadName which looks like "[Get-LogColor]" + .OUTPUTS + [string] + .NOTES + Put this right at the top of function and take the $logLead as input to this. + #> + [OutputType([string])] + param( + [string]$seed + ) + + # length of enum + $colorEnum = [Enum]::GetValues([System.ConsoleColor]) + + # Convert string to byte array, may want to change based on input collation + $bytes = [System.Text.Encoding]::UTF8.GetBytes($seed) + + [uint32]$hash = 0 + + foreach ($octet in $bytes) { + $hash = $hash + $octet + # Get-random only takes uint32, so if it is greater, reset it + if ($hash -ge 4294967295){ + $hash = $octet + } + } + # Get a 'random' number to use as array index + $random = get-random -SetSeed $hash -Minimum 0 -Maximum $colorEnum.length + $color = $colorEnum[$random] + + return $color +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-LogLeadName.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-LogLeadName.ps1 new file mode 100644 index 0000000..9de1aca --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-LogLeadName.ps1 @@ -0,0 +1,13 @@ +function Get-LogLeadName{ +<# +.SYNOPSIS + Get the name of the function that called for the log lead so we consistently define it. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + $command = (Get-PSCallStack)[1].Command + return "[$command]"; +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-MachineConfigAppSetting.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-MachineConfigAppSetting.ps1 new file mode 100644 index 0000000..df9f374 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-MachineConfigAppSetting.ps1 @@ -0,0 +1,48 @@ +function Get-MachineConfigAppSetting { +<# +.SYNOPSIS + Returns an app setting value from the machine config +#> + param ( + [Parameter(Position = 0, Mandatory = $true)] + [Alias("Key")] + [string]$appSettingKey + ) + + Write-Warning "This function to be deprecated in favor of Get-AppSetting" + try { + Write-Host "##teamcity[message text='Found usage of Get-MachineConfigAppSetting. This should be switched to Get-AppSetting. Please address in $(Get-ParentExecutionName)' status='WARNING']" + } catch { + ## In case the Get-ParentExecutionName call blows up. It shouldn't. + ## But I'm calling this to deprecate the usage of this function, so I don't want this to break that ... yet + Write-Host "##teamcity[message text='Found usage of Get-MachineConfigAppSetting. This should be switched to Get-AppSetting. Please address in the parent call.' status='WARNING']" + } + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : Reading machine.config") + $machineConfig = Read-MachineConfig + + if ($null -eq $machineConfig) { + Write-Warning ("$logLead : The machine.config file could not be read") + return $null + } + + $appSettingsNode = $machineConfig.Configuration.appSettings + + if ($null -eq $appSettingsNode) { + Write-Warning ("$logLead : The app settings section could not be found") + return $null + } + + Write-Verbose ("$logLead : Looking for AppSetting with Key {0}" -f $appSettingKey) + $appSetting = $appSettingsNode.SelectNodes("//add[@key='$appSettingKey']") + + if (($null -eq $appSetting) -or ($appSetting.Count -eq 0)) { + Write-Warning ("$logLead : The AppSetting with Key {0} could not be found" -f $appSettingKey) + return $null + } + + return $appSetting +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyDecryptionKey.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyDecryptionKey.ps1 new file mode 100644 index 0000000..b4f6b71 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyDecryptionKey.ps1 @@ -0,0 +1,21 @@ +function Get-MachineKeyDecryptionKey { + <# +.SYNOPSIS + Gets the machine key decryption key +#> + [CmdletBinding()] + Param() + if ($machineKeyDecryptionKey -ne "2701BE9B42AAC5769232FFFE894C086B6838C613481B2694C975BCAC02807BD6") { + return $machineKeyDecryptionKey + } + + $validationKeyString = ("{0}IlooktotheseareflectionsinthewavessparkmymemorySomehappysomesadIthinkofchildhoodfriendsandthedreamswehadWelivehappilyforeversothestorygoesButsomehowwemissedoutonthatpotofgoldButwelltrybestthatwecantocarryon" -f [Environment]::GetEnvironmentVariable("POD", "Machine")).Substring(0, 64) + $result = Get-Sha256Hash -value $validationKeyString + + do { + $result += Get-Sha256Hash -value $validationKeyString + } + while ($result.Length -lt 64) + + return $result.SubString(0, 64) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyValidationKey.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyValidationKey.ps1 new file mode 100644 index 0000000..1032a79 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-MachineKeyValidationKey.ps1 @@ -0,0 +1,21 @@ +function Get-MachineKeyValidationKey { + <# +.SYNOPSIS + Gets the machine key validation key +#> + + if ($machineKeyValidationKey -ne "C153B7375BE81D1B1F01D9AB2F8ED31E4CECD7A7EA226DF91EF737E0C3E5A081D07B1883BA80B866EF666B837D839A0739E22506F044148CF8F35854A3CD0472") { + return $machineKeyValidationKey + } + + $machineKeyString = ("{0}ImsailingawaysetanopencourseforthevirginseaIvegottobefreefreetofacethelifethatsaheadofmeOnboardImthecaptainsoclimbaboardWellsearchfortomorrowoneveryshoreAndIlltryohLordIlltrytocarryon" -f [Environment]::GetEnvironmentVariable("POD", "Machine")).Substring(0, 128) + $result = Get-Sha256Hash -value $machineKeyString + + do { + $result += Get-Sha256Hash -value $machineKeyString + } + while ($result.Length -lt 128) + + return $result.SubString(0, 128) +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-MasterConnectionString.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-MasterConnectionString.ps1 new file mode 100644 index 0000000..c6fad2b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-MasterConnectionString.ps1 @@ -0,0 +1,17 @@ +function Get-MasterConnectionString { +<# +.SYNOPSIS + Returns the Master Database Connection String +.DESCRIPTION + Reads the value from the appSetting named "AlkamiMaster" from the machine.config ConnectionStrings block +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Justification="Array Consolidation is Acceptable")] + param() + + $machineConfig = Read-MachineConfig + $masterConnectionString = $machineConfig.Configuration.ConnectionStrings.ChildNodes | Where-Object {$_.Name -eq "AlkamiMaster"} | Select-Object -ExpandProperty connectionString + + return ($masterConnectionString, $global:masterConnectionString -ne $null)[0]; +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.ps1 new file mode 100644 index 0000000..2e743cc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.ps1 @@ -0,0 +1,280 @@ +function Get-NewRelicApmUpgradeData { + + <# +.SYNOPSIS +Compares the current version of NewRelic APM to the expected version, and returns an object describing if an upgrade or downgrade is required. + +.DESCRIPTION +Compares the current version of NewRelic APM to the expected version, and returns an object describing if an upgrade or downgrade is required. Uses either +the supplied desired version value OR the calculated value based on the deployed ORB version, if applicable. + +.PARAMETER DesiredAPMVersion +Optional parameter indicating the desired APM version. When not supplied, will calculate the desired version based on deployed ORB version + +.PARAMETER SourceFeed +Optional parameter. If supplied, should be the full soruce URL for the desired Chocolatey feed to pull the newrelic-dotnet package froms + +.PARAMETER NewRelicPackageName +Optional parameter, defaults to "newrelic-dotnet". Should be set to the NewRelic APM MSI-based chocolatey package you want to install + +.OUTPUTS +An object which contains the below properties +- ActionRequired [Boolean] : Indicates if an upgrade or downgrade is required +- ActionType [String] : Value should be one of Upgrade or Downgrade +- TargetVersion [System.Version] : The dervied version for install +- SourceFeed [String] : The feed URL which should be used for install +.Example +e + +#> + [CmdletBinding()] + [OutputType([System.Object])] + param( + [Parameter(Mandatory = $false)] + [System.Version]$DesiredAPMVersion = $null, + + [Parameter(Mandatory = $false)] + [string]$SourceFeed = $null, + + [Parameter(Mandatory = $false)] + [string]$NewRelicPackageName = "newrelic-dotnet" + ) + + + #TODO: Add -ComputerName to allow remote-running this? + + + $alkamiFeedMatch = "http?://packagerepo.orb.alkamitech.com/nuget/choco.*" + $alkamiFeedBlockExpression = "choco.sre|choco.internal" + + $logLead = Get-LogLeadName + + $hostname = Get-FullyQualifiedServerName + + $newrelicApmUpgradelData = @{ + ComputerName = $hostname + ActionRequired = $false + ActionType = $null + TargetVersion = $null + FeedUrl = $null + FoundPackage = $null + } + $takeAction = $null + + if (Test-StringIsNullOrEmpty -Value $DesiredAPMVersion) { + # No target version was supplied, lets see if we can figure it out from what's deployed + Write-Verbose "$logLEad : No DesiredAPMVersion supplied. Will attempt to determine target version based on deployed platform version" + $deployedPlatformVersion = Get-OrbVersion + + if (-NOT (Test-StringIsNullOrEmpty -Value $deployedPlatformVersion)) { + $targetAPMVersion = Get-SupportedPlatformAPMVersion -PlatformVersion $deployedPlatformVersion + } + + } else { + # Lets assume the person calling the function knows what they want + $targetAPMVersion = $DesiredAPMVersion + } + + if ($null -eq $targetAPMVersion) { + # Since no version was supplied, we will use the latest on the feed. This may be the case for non-ORB servers where they just want whatever + # is appropriate, and not a specific version + Write-Host "$logLead : Could not determine the appropriate target APM version. The latest version in the configured feeds will be used" + } + + # Print what we're going to compare against + $targetVersionString = Test-IsNull $targetAPMVersion "LATEST AVAILABLE" -Strict + + Write-Host "$logLead : Evaluating current state vs. desired APM version [$targetVersionString]" + + # Get the local NewRelic APM version + $localChoco = Get-ChocoState -LocalOnly -Exact -PackageName $NewRelicPackageName + + if ($null -eq $localChoco) { + Write-Host "$logLead : No local NewRelic APM choco package detected." + } else { + $localChocoVersion = $localChoco.Version + Write-Host "$logLead : Detected local NewRelic APM choco package version: [$localChocoVersion]" + } + + # Figure out where we're installing NewRelic APM from, if not supplied by the caller + #region FindPackage + [hashtable]$foundPackage = @{ + PackageId = $null + PackageVersion = $null + Source = $null + FeedUrl = $null + } + + if (Test-StringIsNullOrEmpty -Value $SourceFeed) { + # Pull the local feeds matching $alkamiFeedMatch + #[array]$sources = Get-ChocolateySources | Where-Object { + # $_.Source -like $alkamiFeedMatch -and + # [bool]$_.IsSDK -eq $false -and + # [bool]$_.Disabled -eq $false + #} + [array]$sources = (Get-ChocolateySources).Where({ + $_.Source -like $alkamiFeedMatch -and + [bool]$_.IsSDK -eq $false -and + [bool]$_.Disabled -eq $false -and + $_.Source -notmatch $alkamiFeedBlockExpression + }) + + if (Test-IsCollectionNullOrEmpty -Collection $sources) { + # (/) TESTCASE - Get-ChocolateySources - return hashtable with 1 item that has a .Source member + # https://packagerepo.orb.alkamitech.com/nuget/choco.sre + # should write this warning and eventually return a ActionRequired=$false object + + # No matching choco sources found. We have to exit early + Write-Warning "$logLead : Could not find a configured feed which matches the requirements for NewRelic APM. Evaluation cannot continue" + $takeAction = $false + + } else { + #TESTCASE - Get-ChocolateySources - return hashtable with 1 item that has a .Source member + # https://packagerepo.orb.alkamitech.com/nuget/choco.good + # should write this verbose and call Get-ChocoState and more + + + # Find the feed with NewRelic APM + foreach ($feedSource in $sources) { + + Write-Verbose "$logLead : Checking source with name [$($feedSource.Name)] and Feed URL [$($feedSource.Source)]" + + # In order to not overwrite this value, we CONTINUE once found + # Get-ChocoState returns an array, even though we often only get 1 result + # sometimes powershell unboxes for us, sometimes it doesn't + # Take no changes, force the Select-Object -First 1 anyway + $availableChocoPackageList = Get-ChocoState -Exact -PackageName $NewRelicPackageName -PackageVersion $targetAPMVersion -Source $feedSource.Source -ErrorAction Continue + + $availableChocoPackage = Select-Object -InputObject $availableChocoPackageList -First 1 + + + if ($null -eq $availableChocoPackage -or (Test-IsCollectionNullOrEmpty -Collection $availableChocoPackage)) { + + Write-Host "$logLead : Package $NewRelicPackageName not found at feed [$($feedSource.Source)]." + + } else { + Write-Host "$logLead : Package $NewRelicPackageName found at $($feedSource.Source). Adding this package with this source to the list" + # The first found package is enough, even if there are other versions on other feeds + # I know Source and FeedUrl are redundant. It is convenient. + $packageSource = $feedSource.Source + $foundPackage.PackageId = $availableChocoPackage.Name + $foundPackage.PackageVersion = [Version]($availableChocoPackage.Version) + $foundPackage.Source = $feedSource + $foundPackage.FeedUrl = $feedSource.Source + continue + + } + } + + if ($null -eq $packageSource) { + + Write-Host "$logLead : No configured feed has package $NewRelicPackageName available. Evaluation cannot continue." + $takeAction = $false + } + } + } + #endregion FindPackage + + # Now the Version Comparison + #region CompareVersions + # Compare-Semver shorthand + # -1 -> first value TRAILS/IS LESS THAN second value + # 0 -> values are equal + # +1 -> first value LEADS/IS MORE THAN second value + # SO + # -1 = should upgrade + # 0 = do nothing + # 1 = should downgrade + $actions = @{ + -1 = "Upgrade" + 0 = "Nothing" + 1 = "Downgrade" + } + + if ($takeAction -eq $false) { + Write-Warning "$loglead : Skipping version comparison due to previous condition preventing action" + $actionValue = 0 + } else { + Write-Host "$loglead : Comparing versions to determine what action to take" + # OK so we have a local NewRelic version already. + if ($null -ne $localChocoVersion) { + #region Have Local Installation + + + # we don't really *need* this if-else.... + # both of them use the $foundPackage anyway because we added -Version + # to Get-ChocoState and we need whatever we found to be installed.... + # so figure out how to not have this `if ($null -eq $targetAPMVersion)` - go off of $foundPackage + # instead + # The insides are basically teh same anyway, right? + # The Write-Hosts are subtly different, but... uh... do we really need them? + if ($null -eq $targetAPMVersion) { + # But we don't have a specific target version. So we need to upgrade/downgrade to the latest, if it's different + + $actionValue = Compare-SemVer -Version1 $localChocoVersion -Version2 $foundPackage.PackageVersion + + if ($actionValue -ne 0) { + + Write-Host "$logLead : A(n) $($actions[$actionValue]) is required because local choco version [$localChocoVersion] is not equivalent to the highest available version on the feeds [$($foundPackage.PackageVersion)]." + $takeAction = $true + $targetAPMVersion = $foundPackage.PackageVersion + } else { + + Write-Host "$logLead : A(n) $($actions[$actionValue]) is not required because local choco version [$localChocoVersion] is equivalent to the highest available version on the feeds [$($foundPackage.PackageVersion)]." + $takeAction = $false + } + } else { + # We DO have a target + $actionValue = Compare-SemVer -Version1 $localChocoVersion -Version2 $targetAPMVersion + + # We do have a specific target version. So let's compare against that + if ($actionValue -ne 0) { + + Write-Host "$logLead : A(n) $($actions[$actionValue]) is required because local choco version [$localChocoVersion] is not equivalent to the target APM version [$targetAPMVersion]." + $takeAction = $true + } else { + + Write-Host "$logLead : A(n) $($actions[$actionValue]) is not required because local choco version [$localChocoVersion] is equivalent to the target APM version [$targetAPMVersion]." + $takeAction = $false + } + } + #endregion Have Local Installation + } else { + #region No Local Installation + + # There is no local choco version. + # The only reason we would return false here is if the calculated target version is not on the feed + if ($null -eq $targetAPMVersion -and $null -eq $foundPackage.PackageVersion) { + + # No version specified, but it wouldn't matter either way. + Write-Host "$logLead : No local NewRelic APM choco package is installed, and no configured feeds host any version of the package [$NewRelicPackageName]. Upgrade/downgrade cannot be performed" + $takeAction = $false + + } + + #if ($null -ne $targetAPMVersion) { + # We have a specific target version. Lets check to see if there + # $foundPackage below handles this. + #} + + if ($null -ne $foundPackage.PackageVersion) { + $actionValue = -1 + Write-Host "$loglead : A(n) $($actions[$actionValue]) is required because no local NewRelic APM choco package is installed and version [$($foundPackage.PackageVersion)] was found on the feed named [$($foundPackage.Source.Name)]" + $takeAction = $true + } + #endregion No Local Installation + } + #endregion CompareVersions + + } + + + + $newrelicApmUpgradelData.ActionRequired = $takeAction + $newrelicApmUpgradelData.ActionType = $actions[$actionValue] + $newrelicApmUpgradelData.TargetVersion = $foundPackage.PackageVersion + $newrelicApmUpgradelData.FeedUrl = $foundPackage.FeedUrl + $newrelicApmUpgradelData.FoundPackage = $foundPackage + # And the result! + return $newrelicApmUpgradelData +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.tests.ps1 new file mode 100644 index 0000000..a0bcba5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-NewRelicApmUpgradeData.tests.ps1 @@ -0,0 +1,207 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NewRelicApmInstallData" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Debug -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {} + + # Assume running commands on localhost unless overridden withing a Context or It + Mock -ModuleName $moduleForMock -CommandName Compare-StringToLocalMachineIdentifiers -MockWith {return $true} + Mock -ModuleName $moduleForMock -CommandName Invoke-CommandWithRetry -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-SupportedPlatformAPMVersion -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith {} + + $validFakeChocoSources = @( + @{ + Name = "ChocoFake1" + Source = "https://packagerepo.orb.alkamitech.com/nuget/choco.fake1" + IsSDK = $false + Disabled = $false + }, + @{ + Name = "ChocoFake2" + Source = "https://packagerepo.orb.alkamitech.com/nuget/choco.fake2" + IsSDK = $false + Disabled = $false + } + ) + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateySources -MockWith { return $validFakeChocoSources } + + # Technically, I should mock this, but... it's being used multiple times to check + # values I have control over, so I'm going to let it go until I hit something I + # don't control. For now. + #Mock -ModuleName $moduleForMock -CommandName Test-StringIsNullOrEmpty -MockWith {} + + Context "Happy Path" { + + # This test is also the "Check most of the Assert-Mocks" test for the happy path + It "No supplied Version, no ORB, APM exists on a feed, return Take-action Upgrade" { + + $nrLocal = @{ + Version = "1.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + $nrRemote = @{ + Version = "2.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + + # LOCAL Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrLocal} -ParameterFilter { $Exact -eq $true -and $LocalOnly -eq $true } + + # REMOTE Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrRemote } -ParameterFilter { $Source -in $validFakeChocoSources.Source } + + $nrApmInstallData = Get-NewRelicApmUpgradeData -Verbose + + + #region Verify a ton of mocks in execution order + Assert-MockCalled -CommandName Get-OrbVersion -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-SupportedPlatformAPMVersion -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + + Assert-MockCalled -CommandName Write-Host -Times 1 -Scope It -ParameterFilter { $Object -match "Could not determine the appropriate target APM version. The latest version in the configured feeds will be used" } + + Assert-MockCalled -CommandName Get-ChocoState -Times 1 -ModuleName $moduleForMock -ParameterFilter { $Exact -eq $true -and $LocalOnly -eq $true } -Scope It + Assert-MockCalled -CommandName Write-Host -Times 1 -ModuleName $moduleForMock -ParameterFilter { $Object -match "Detected local NewRelic APM choco package version: \[$($nrLocal.Version)\]" } -Scope It + + Assert-MockCalled -CommandName Write-Warning -Times 0 -ModuleName $moduleForMock -ParameterFilter { $Message -match " Could not find a configured feed which matches the requirements for NewRelic APM. Evaluation cannot continue" } -Scope It + + Assert-MockCalled -CommandName Write-Verbose -Times 1 -ModuleName $moduleForMock -ParameterFilter { $Message -match "Checking source with name \[$($validFakeChocoSources[0].Name)\] and Feed URL \[$($validFakeChocoSources[0].Source)\]" } -Scope It + + Assert-MockCalled -CommandName Get-ChocoState -Times 1 -ModuleName $moduleForMock -ParameterFilter { $Source -in $validFakeChocoSources.Source } -Scope It + #endregion Verify a ton of mocks in execution order + + $nrApmInstallData.ActionRequired | Should -BeTrue + + } + + # This test is also in the catch-all baseline above, but it is also a + # separate bullet in the story. It warrants its own test for visibility + It "No supplied Version, determine the correct version based on the ORB " { + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { [System.Version]"1.0.0.0" } + + $nrApmInstallData = Get-NewRelicApmUpgradeData + + Assert-MockCalled -CommandName Get-OrbVersion -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Get-SupportedPlatformAPMVersion -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + # This is purely to make the "variable is declared and never used" squiggles go away + $nrApmInstallData | Should -Not -BeNullOrEmpty + + } + + It "APM Version supplied, APM Version on feed, Different APM version local, No ORB, return Take-Action Upgrade" { + + $nrLocal = @{ + Version = "1.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + $nrRemote = @{ + Version = "2.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + + # LOCAL Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrLocal } -ParameterFilter { $Exact -eq $true -and $LocalOnly -eq $true } + + # REMOTE Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrRemote } -ParameterFilter { $Source -in $validFakeChocoSources.Source } + + $nrApmInstallData = Get-NewRelicApmUpgradeData -DesiredAPMVersion 2.0.0.0 + + $nrApmInstallData.ActionRequired | Should -BeTrue + $nrApmInstallData.ActionType | Should -Be "Upgrade" + + } + + It "No supplied version, ORB, Supported version on feed, Lower than installed version, return Take-Action Downgrade" { + $nrLocal = @{ + Version = "2.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + $nrRemote = @{ + Version = "1.0.0.0" + Name = "newrelic-dotnet" + Feed = $null + } + + # LOCAL Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrLocal } -ParameterFilter { $Exact -eq $true -and $LocalOnly -eq $true } + + # REMOTE Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrRemote } -ParameterFilter { $Source -in $validFakeChocoSources.Source } + + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { return [System.Version]"1.0.0.0" } + Mock -ModuleName $moduleForMock -CommandName Get-SupportedPlatformAPMVersion -MockWith { return "1.0.0.0" } + + $nrApmInstallData = Get-NewRelicApmUpgradeData + + $nrApmInstallData.ActionRequired | Should -BeTrue + $nrApmInstallData.ActionType | Should -Be "Downgrade" + + } + + } + + Context "Unappy Path" { + + It "NO valid Feeds Warns, Returns No-Action" { + $nonValidFakeChocoSources = @( + @{ + Name = "BadChocoFake1" + Source = "https://packagerepo.orb.alkamitech.com/nuget/choco.sre1" + IsSDK = $false + Disabled = $false + } + ) + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateySources -MockWith { return $nonValidFakeChocoSources } + + $nrApmInstallData = Get-NewRelicApmUpgradeData + + Assert-MockCalled -CommandName Write-Warning -Times 1 -ModuleName $moduleForMock -ParameterFilter { $Message -match " Could not find a configured feed which matches the requirements for NewRelic APM. Evaluation cannot continue" } + + $nrApmInstallData.ActionRequired | Should -BeFalse + + + } + It "No APM on valid feed, No local APM, ORB installed, returns No-Action" { + $nrRemote = @{} + $nrLocal = @{} + Mock -ModuleName $moduleForMock -CommandName Get-OrbVersion -MockWith { return [System.Version]"1.0.0.0" } + Mock -ModuleName $moduleForMock -CommandName Get-SupportedPlatformAPMVersion -MockWith { return "1.0.0.0" } + # LOCAL Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrLocal } -ParameterFilter { $Exact -eq $true -and $LocalOnly -eq $true } + + # REMOTE Get-ChocoState + Mock -ModuleName $moduleForMock -CommandName Get-ChocoState -MockWith { return $nrRemote } -ParameterFilter { $Source -in $validFakeChocoSources.Source } + + $nrApmInstallData = Get-NewRelicApmUpgradeData + + $nrApmInstallData.ActionRequired | Should -BeFalse + + + } + + + } + + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-OrbLogsPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-OrbLogsPath.ps1 new file mode 100644 index 0000000..4c005bb --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-OrbLogsPath.ps1 @@ -0,0 +1,21 @@ +function Get-OrbLogsPath { +<# +.SYNOPSIS + Get the path to the root of the Orb logging folders + +.NOTES + This function does not have a unit test because this just returns a Join-Path +#> + [CmdletBinding()] + [OutputType([string])] + Param() + + # This called function always returns what should be a valid value. + # If the path does not exist, consumers should fail. + # This is just a shim for Join-Path to allow for consistency. + $installationDrive = Get-AlkamiInstallationDrive + $orbLogsPath = Join-Path -Path $installationDrive -ChildPath 'OrbLogs' + + return $orbLogsPath +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-OrbPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-OrbPath.ps1 new file mode 100644 index 0000000..9613eee --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-OrbPath.ps1 @@ -0,0 +1,20 @@ +function Get-OrbPath { +<# +.SYNOPSIS + Get the path to the root of the Orb WCF/Client folders + +.NOTES + This function does not have a unit test because this just returns a Join-Path +#> + [CmdletBinding()] + [OutputType([string])] + Param() + + # This called function always returns what should be a valid value. + # If the path does not exist, consumers should fail. + # This is just a shim for Join-Path to allow for consistency. + $installationDrive = Get-AlkamiInstallationDrive + $orbPath = Join-Path -Path $installationDrive -ChildPath 'Orb' + + return $orbPath +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-OrbSharedPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-OrbSharedPath.ps1 new file mode 100644 index 0000000..d8798a6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-OrbSharedPath.ps1 @@ -0,0 +1,11 @@ +function Get-OrbSharedPath { +<# +.SYNOPSIS + Get the path to the root of the Orb WCF/Client folders +#> + [CmdletBinding()] + Param() + + return (Join-Path (Get-OrbPath) Shared); +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-OrbVersion.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-OrbVersion.ps1 new file mode 100644 index 0000000..ddd8668 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-OrbVersion.ps1 @@ -0,0 +1,42 @@ +function Get-OrbVersion { +<# +.SYNOPSIS + Returns the version of orb, as read from the known version.txt file location. + +.PARAMETER ComputerName + The computer name to retrieve the file from. If it is empty, will be the local machine. + +.OUTPUTS + Can return null if the path can not be read +#> + [CmdletBinding()] + [OutputType([string])] + Param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + $logLead = (Get-LogLeadName) + + $versionPath = (Join-Path(Get-OrbPath) "WebClient/version.txt") + + if (![string]::IsNullOrWhiteSpace($ComputerName)) { + if(!(Compare-StringToLocalMachineIdentifiers -StringToCheck $ComputerName)) { + $versionPath = (Get-UncPath -filePath $versionPath -ComputerName $ComputerName) + } + } + + if(Test-Path $versionPath) + { + $versionTxt = (Get-Content $versionPath) + # this value is programmatically produced to always be + # the git hash of the branch being built, and the build number + # so we always can safely split on a space + $versionTxt = ($versionTxt.Split(" ")[1]) + return $versionTxt + } + else + { + Write-Warning "$logLead - '$versionPath' was not found." + return $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionName.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionName.ps1 new file mode 100644 index 0000000..192ed1d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionName.ps1 @@ -0,0 +1,11 @@ +function Get-ParentExecutionName { +<# +.SYNOPSIS + Get the name of the parent executing function +#> + [CmdletBinding()] + Param() + + return (Get-Variable MyInvocation -Scope 2).Value.MyCommand.Name +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionNameExampleUsage.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionNameExampleUsage.ps1 new file mode 100644 index 0000000..d8f9f96 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ParentExecutionNameExampleUsage.ps1 @@ -0,0 +1,11 @@ +function Get-ParentExecutionNameExampleUsage { +<# +.SYNOPSIS + Demo getting the parent executing name +#> + [CmdletBinding()] + Param() + + (Get-ParentExecutionName); +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-PasswordFromCredential.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-PasswordFromCredential.ps1 new file mode 100644 index 0000000..e2c08c1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-PasswordFromCredential.ps1 @@ -0,0 +1,27 @@ +Function Get-PasswordFromCredential { + <# + .SYNOPSIS + Gets the password from a credential object. Allows us to regulate when and where we get passwords and to pass credentials around the application. + + .PARAMETER Credential + [PSCredential] The credential with username and password configured. + #> + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)] + [PSCredential]$Credential + ) + + $tValue1 = $null + + $bstr1 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password); + try + { + $tValue1 = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr1) + } + finally + { + [Runtime.InteropServices.Marshal]::FreeBSTR($bstr1); + } + return $tValue1 +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecretServerUri.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecretServerUri.ps1 new file mode 100644 index 0000000..d410550 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecretServerUri.ps1 @@ -0,0 +1,12 @@ +function Get-SecretServerUri { +<# +.SYNOPSIS + Get the base URI of the secret server. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + return "https://alkami.secretservercloud.com"; +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.ps1 new file mode 100644 index 0000000..fbb6fc8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.ps1 @@ -0,0 +1,20 @@ +Function Get-SecureString { + <# + .SYNOPSIS + Convert a string to a SecureString. This function is to allow for mocking, and to allow for auditing of the usage of SecureStrings. + + .PARAMETER String + The string to be converted. This name corresponds with ConvertTo-SecureString parameters. + #> + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)] + [string]$String + ) + + $secureString = New-Object SecureString + foreach($char in $String.ToCharArray()) { + $secureString.AppendChar($char) + } + return $secureString +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.tests.ps1 new file mode 100644 index 0000000..07ca86f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecureString.tests.ps1 @@ -0,0 +1,54 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +## https://stackoverflow.com/questions/4502676/c-sharp-compare-two-securestrings-for-equality +## SecureStringToBSTR has a SecurityCriticalAttribute so it requires full trust for the immediate caller. This member cannot be used by partially trusted or transparent code. +## https://referencesource.microsoft.com/#mscorlib/system/security/attributes.cs,29a3d687a50338b1 +function Compare-TwoSecureStrings($secureString1, $secureString2) +{ + $bstr1 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString1); + $bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString2); + $result = $false; + try + { + $tValue1 = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr1) + $tValue2 = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr2) + ## This function can literally deconvert passwords, use this knowledge with extreme care + ## Write-Host $tValue1 + ## Write-Host $tValue2 + $result = $tValue1 -eq $tValue2 + } + finally + { + [Runtime.InteropServices.Marshal]::FreeBSTR($bstr1); + [Runtime.InteropServices.Marshal]::FreeBSTR($bstr2); + } + return $result +} + +Describe 'Get-SecureString' { + Context 'Ensure value returned matches default implementation' { + It 'Use naive password "password"' { + $defaultString = "password" + $builtinValue = ConvertTo-SecureString -String $defaultString -AsPlainText -Force + $testValue = Get-SecureString -String $defaultString + $testResult = (Compare-TwoSecureStrings $builtinValue $testValue) + $testResult | Should -Be $true + } + + It 'Use two different passwords to ensure this is broken when doing so' { + $defaultString1 = "password1" + $defaultString2 = "password2" + $builtinValue = ConvertTo-SecureString -String $defaultString1 -AsPlainText -Force + $testValue = Get-SecureString -String $defaultString2 + $testResult = (Compare-TwoSecureStrings $builtinValue $testValue) + $testResult | Should -Be $false + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicy.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicy.ps1 new file mode 100644 index 0000000..7b53d62 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicy.ps1 @@ -0,0 +1,17 @@ +function Get-SecurityPolicy { +<# +.SYNOPSIS + Gets the security policy and returns the full content. +#> + param( + + ) + + $logLead = (Get-LogLeadName); + $exportFile = [System.IO.Path]::GetTempFileName() + + Write-Verbose ("$logLead : Exporting local security policy to {0}" -f $exportFile) + secedit.exe /export /cfg $($exportFile) | Out-Null + + return ( Get-Content -Path $exportFile ) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.ps1 new file mode 100644 index 0000000..44d7649 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.ps1 @@ -0,0 +1,19 @@ +function Get-SecurityPolicySetting { +<# +.SYNOPSIS + Gets the security policy Value for a given setting. +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$settingName + ) + + $logLead = (Get-LogLeadName) + Write-Verbose "$logLead : Getting Security policy" + $securityContent = Get-SecurityPolicy + Write-Verbose "$logLead : Parsing Security policy" + + return ($securityContent | Where-Object {$_ -like ("{0}*" -f $settingName)} | ForEach-Object {$_.Split("=", [System.StringSplitOptions]::RemoveEmptyEntries).Trim()} | Select-Object -Last 1) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.tests.ps1 new file mode 100644 index 0000000..1f2dcf2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SecurityPolicySetting.tests.ps1 @@ -0,0 +1,34 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe Get-SecurityPolicySetting { + + $mockedLogonSID = "S-1-2-3-420-8675309" + $mockedProfileSID = "S-6-6-6-666666" + + mock -ModuleName $moduleForMock Get-SecurityPolicy { + $mockedLogonSID = "S-1-2-3-420-8675309" + $mockedProfileSID = "S-6-6-6-666666" + $mockedSecurityPolicyContent = @('[Privilege Rights]', "SeServiceLogonRight = *S-1-1-1-11111,*$mockedLogonSID", "SeSystemProfilePrivilege = *S-1-1-1-11111,*$mockedProfileSID") + return $mockedSecurityPolicyContent + } + + Context "Returns a SID for a given setting" { + + it "Returns SID for SeServiceLogonRight" { + $result = Get-SecurityPolicySetting -settingName "SeServiceLogonRight" + + $result | should -BeLike *$mockedLogonSID* + } + it "Returns SID for SeSystemProfilePrivilege" { + $result = Get-SecurityPolicySetting -settingName "SeSystemProfilePrivilege" + + $result | should -BeLike *$mockedProfileSID* + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.ps1 new file mode 100644 index 0000000..a7833f0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.ps1 @@ -0,0 +1,31 @@ +function Get-ServerByType { +<# +.SYNOPSIS +Filters a list of servers by type using hostname convention. + +.DESCRIPTION +This function is a wrapper around the Select-Alkami*Servers functions. + +.PARAMETER Server +An array of strings representing the hostnames of servers to be filtered. Mandatory. + +.PARAMETER Type +A string indicating which server Type to return. Must be one of "App", "Fab", "Mic", "Web". Mandatory. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string[]]$Server, + [Parameter(Mandatory=$true)] + [ValidateSet("App", "Fab", "Mic", "Web")] + [string]$Type + ) + $results = @() + switch ($Type) { + "App" { $results = Select-AlkamiAppServers -servers $Server} + "Fab" { $results = Select-AlkamiFabServers -servers $Server} + "Mic" { $results = Select-AlkamiMicServers -servers $Server} + "Web" { $results = Select-AlkamiWebServers -servers $Server} + } + return $results +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.tests.ps1 new file mode 100644 index 0000000..fc6b785 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ServerByType.tests.ps1 @@ -0,0 +1,33 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Call wrapped functions" { + Mock -ModuleName $moduleForMock Select-AlkamiAppServers {} #-ParameterFilter {$Type -eq "App"} + Mock -ModuleName $moduleForMock Select-AlkamiFabServers {} #-ParameterFilter {$Type -eq "Fab"} + Mock -ModuleName $moduleForMock Select-AlkamiMicServers {} #-ParameterFilter {$Type -eq "Mic"} + Mock -ModuleName $moduleForMock Select-AlkamiWebServers {} #-ParameterFilter {$Type -eq "Web"} + + $serverList = @("app1","app2","fab1","fab2","mic1","mic2","web1","web2") + + It "Type App" { + $retval = Get-ServerByType -Server $serverList -Type "App" + Assert-MockCalled -CommandName Select-AlkamiAppServers -ModuleName $moduleForMock -Scope It -Exactly 1 + } + It "Type Fab" { + $retval = Get-ServerByType -Server $serverList -Type "Fab" + Assert-MockCalled -CommandName Select-AlkamiFabServers -ModuleName $moduleForMock -Scope It -Exactly 1 + } + It "Type Mic" { + $retval = Get-ServerByType -Server $serverList -Type "Mic" + Assert-MockCalled -CommandName Select-AlkamiMicServers -ModuleName $moduleForMock -Scope It -Exactly 1 + } + It "Type Web" { + $retval = Get-ServerByType -Server $serverList -Type "Web" + Assert-MockCalled -CommandName Select-AlkamiWebServers -ModuleName $moduleForMock -Scope It -Exactly 1 + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ServerTypeByHostname.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ServerTypeByHostname.ps1 new file mode 100644 index 0000000..94438bc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ServerTypeByHostname.ps1 @@ -0,0 +1,31 @@ +function Get-ServerTypeByHostname { +<# +.SYNOPSIS + Returns the type of the server by name. Web, App, Mic, Fab + +.DESCRIPTION + This function is a wrapper around the Select-Alkami*Servers functions. + +.PARAMETER Server + The string to get the server type string from. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory=$true)] + [string]$ComputerName + ) + + if($null -ne (Select-AlkamiWebServers -servers $ComputerName)) { + return "Web"; + } elseif($null -ne (Select-AlkamiAppServers -servers $ComputerName)) { + return "App"; + } elseif($null -ne (Select-AlkamiMicServers -servers $ComputerName)) { + return "Mic"; + } elseif($null -ne (Select-AlkamiFabServers -servers $ComputerName)) { + return "Fab"; + } else { + $loglead = (Get-LogLeadName) + throw "$loglead Server type not recognized for ComputerName $ComputerName"; + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SetDifference.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SetDifference.ps1 new file mode 100644 index 0000000..d713d49 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SetDifference.ps1 @@ -0,0 +1,40 @@ +function Get-SetDifference { +<# +.SYNOPSIS + Returns List $a minus the entries of List $b +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [AllowNull()] + [object]$a, + [Parameter(Mandatory=$true)] + [AllowNull()] + [object]$b + ) + + if(!$a) + { + return $null + } + if(!$b) + { + return $a + } + + $results = @(); + $map = @{}; + + foreach($key in $b) { + $map.Add($key, '1') + } + + foreach($key in $a) { + if(!($map.ContainsKey($key))) { + $results += $key + } + } + + return $results +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-Sha256Hash.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-Sha256Hash.ps1 new file mode 100644 index 0000000..742d243 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-Sha256Hash.ps1 @@ -0,0 +1,35 @@ +function Get-Sha256Hash { + <# +.SYNOPSIS + Get the SHA-256 hash of a value + +.PARAMETER Value + Value to hash +#> + [CmdletBinding()] + Param( + [string]$Value + ) + + [System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null + + try { + $hash = [System.Security.Cryptography.SHA256Managed]::Create() + $enc = [System.Text.Encoding]::UTF8 + $bytes = ($hash.ComputeHash($enc.GetBytes($Value))) + + $machineKeySb = New-Object System.Text.StringBuilder(256) + + for ($i = 0; $i -lt $bytes.Length; $i++) { + $machineKeySb.Append(("{0:X2}" -f $bytes[$i])) | Out-Null + } + + return $machineKeySb.ToString() + } + finally { + if ($null -ne $hash) { + $hash.Dispose() + } + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SidFromUsername.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SidFromUsername.ps1 new file mode 100644 index 0000000..0def00a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SidFromUsername.ps1 @@ -0,0 +1,35 @@ +function Get-SidFromUsername { +<# +.SYNOPSIS + Returns a domain or local user's SID based on username +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [Alias("User")] + [string]$userName, + + [Parameter(Mandatory=$false)] + [Alias("Domain")] + [string]$domainName + ) + + $logLead = (Get-LogLeadName); + + if ([String]::IsNullOrEmpty($domainName)) + { + Write-Verbose ("$logLead : Looking for local user account {0}" -f $userName) + $objUser = New-Object System.Security.Principal.NTAccount($userName) + } + else + { + Write-Verbose ("$logLead : Looking for domain user account {0} in domain {1}" -f $userName, $domainName) + $objUser = New-Object System.Security.Principal.NTAccount($domainName, $userName) + } + + Write-Verbose "$logLead : Translating user to SecurityIdentifier" + $strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier]) + return $strSID.Value +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.ps1 new file mode 100644 index 0000000..da57267 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.ps1 @@ -0,0 +1,141 @@ +function Get-SupportedPlatformAPMVersion { + +<# +.SYNOPSIS + Returns the Supported NewRelic APM Version Based on Alkami Platform Version + +.DESCRIPTION + Returns the Supported NewRelic APM Version Based on Alkami Platform Version. The Alkami Platform includes NewRelic + API libraries which may require specific corresponding versions of APM, without which, the application may not start + or report metrics correctly. + +.PARAMETER PlatformVersion + The Platform Version to evaluate. If not supplied, will use the local or remote ORB version for lookup purposes. If less than a + 4 digit version is supplied, the end will be padded with .0 to make it 4 segments + +.PARAMETER ComputerName + The remote server name to interrogate for deployed ORB version. The default is the local computer. + +.Example + Get-SupportedPlatformAPMVersion + + [Get-SupportedPlatformAPMVersion] : Attempting to Retrieve Alkami Platform Version from LocalHost + [Get-SupportedPlatformAPMVersion] : Based on Alkami Platform version 2022.2.0.13, NewRelic APM version 8.39.0.0 should be installed + 8.39.0.0 + +.Example + Get-SupportedPlatformAPMVersion -PlatformVersion "2022.2.5.7" + + [Get-SupportedPlatformAPMVersion] : Based on Alkami Platform version 2022.2.5.7, NewRelic APM version 8.39.0.0 should be installed + 8.39.0.0 + +.Example + Get-SupportedPlatformAPMVersion -ComputerName "APP123456.domain.local" + + [Get-SupportedPlatformAPMVersion] : Attempting to Retrieve Alkami Platform Version from APP123456.fh.local + [Get-SupportedPlatformAPMVersion] : Based on Alkami Platform version 2022.2.0.13, NewRelic APM version 8.39.0.0 should be installed + 8.39.0.0 + +.Example + Get-SupportedPlatformAPMVersion -PlatformVersion "2022.2" + + [Get-SupportedPlatformAPMVersion] : Less than a 4 digit version detected, only found 2 digits. Padding with '.0' 2 times. + [Get-SupportedPlatformAPMVersion] : Based on Alkami Platform version 2022.2.0.0, NewRelic APM version 8.39.0.0 should be installed + 8.39.0.0 +#> + + [CmdletBinding()] + [OutputType([System.String])] + [CmdletBinding(DefaultParameterSetName = 'LocalHostParameterSet')] + param( + [Parameter(ParameterSetName = 'UserInputParameterSet', Mandatory = $true)] + [System.Version]$PlatformVersion, + + [Parameter(ParameterSetName = 'LocalHostParameterSet', Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME + ) + + $logLead = Get-LogLeadName + + if (-NOT [String]::IsNullOrEmpty($PlatformVersion)) { + + Write-Verbose "$logLead : Using user supplied version $PlatformVersion" + $targetPlatformVersion = $PlatformVersion + + } else { + + $isLocalRun = Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName + + if ($isLocalRun) { + + Write-Host "$logLead : Attempting to Retrieve Alkami Platform Version from LocalHost" + $targetPlatformVersion = Get-ORBVersion + + } else { + + Write-Host "$logLead : Attempting to Retrieve Alkami Platform Version from $ComputerName" + try { + $targetPlatformVersion = Invoke-CommandWithRetry -MaxRetries 3 -Exponential -ScriptBlock { + + Invoke-Command -ComputerName $ComputerName -ScriptBlock { Get-ORBVersion } + } + } catch { + Write-Warning "$logLead : Unable to retrieve remote platform version within the retry limit. Review the logs and address." + } + } + } + + if ($null -eq $targetPlatformVersion) { + + Write-Warning "$logLead : Could not retrieve Alkami Platform version. Returning null" + return $null + } + + $versionDigits = $targetPlatformVersion.ToString().Split('.').Count + if ($versionDigits -lt 4) { + + $missingDigits = 4 - $versionDigits + Write-Host "$logLead : Less than a 4 digit version detected, only found $versionDigits digits. Padding with '.0' $missingDigits times." + + $i = 0 + do { + + $targetPlatformVersion = $targetPlatformVersion.ToString() + ".0" + $i++ + } while ($i -lt $missingDigits) + + Write-Verbose "$logLead : Final target version is $targetPlatformVersion" + } + + $map = Get-SupportedPlatformAPMVersionMap + + # Lord help us if the Platform version ever gets this high + $supermaxVersion = ("{0}.{0}.{0}.{0}" -f [Int]::MaxValue) + + [PSObject[]]$targetAPMVersion = $map | Where-Object { + + # Compare the minimum version (if $null, then 0.0.0.0) from the map to the ORB version to find an equivilent or higher match + (Compare-SemVer -Version1 $targetPlatformVersion -Version2 (Test-IsNull -ValueA $_.PlatformMinimumVersion -ValueB "0.0.0.0" -Strict)) -ge 0 -and + + # Compare the maximum version (if $null, then [int32.maxvalue].0.0) from the map to the ORB version to find a lower match + (Compare-SemVer -Version1 $targetPlatformVersion -Version2 (Test-IsNull -ValueA $_.PlatformMaximumVersion -ValueB $supermaxVersion -Strict)) -le 0 + } + + # If we somehow matched more than one APM version from the matrix, someone made a booboo + # Hopefully the tests prevent that from being merged, but just in case + if ($targetAPMVersion.Count -gt 1) { + + Write-Warning "$logLead : More than one NewRelic APM version matched to the current ORB version. This should not be possible, and so something is horribly wrong. Check the array in Get-SupportedPlatformAPMVersionMap for mistakes" + Write-Warning "$logLead : Matched the Below:" + + foreach ($targetVersion in $targetAPMVersion) { + + Write-Warning "$logLead : $($targetVersion.APMVersion)" + } + + return $null + } + + Write-Host "$logLead : Based on Alkami Platform version $targetPlatformVersion, NewRelic APM version $($targetAPMVersion.APMVersion) should be installed" + return ([PSObject]$targetAPMVersion).APMVersion +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.tests.ps1 new file mode 100644 index 0000000..7b94598 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersion.tests.ps1 @@ -0,0 +1,132 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-SupportedPlatformAPMVersion" { + + Context "Unhappy Path" { + + It "Writes a Warning and Returns Null if More Than One APM Version Match Found" { + + Mock -ModuleName $moduleForMock -CommandName Get-SupportedPlatformAPMVersionMap -MockWith { + + return @( + @{ + APMVersion = "9.1.1.0"; + PlatformMinimumVersion = "0.0.0.0"; + PlatformMaximumVersion = "2.0.0.0"; + }, + @{ + APMVersion = "9.2.2.0"; + PlatformMinimumVersion = "0.0.0.0"; + PlatformMaximumVersion = "2.0.0.0"; + } + ) + } + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Get-SupportedPlatformAPMVersion -PlatformVersion "1.0.0.0" | Should -BeNullOrEmpty + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Times 1 -ParameterFilter { + + $Message -match "More than one NewRelic APM version matched" + } + } + + It "Writes a Warning and Returns Null if Remote Version Retrieval Fails" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith { return $null } + Get-SupportedPlatformAPMVersion -ComputerName "iDontExist.nowhere.org" | Should -BeNullOrEmpty + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Times 1 -ParameterFilter { + + $Message -match "Could not retrieve Alkami Platform version" + } + } + + It "Throws if a User Specifies Both Remote Retrieval and Platform Version" { + + { Get-SupportedPlatformAPMVersion -PlatformVersion "1.0.0.0" -ComputerName "fakemachine.lulwut.com" } | Should -Throw + } + } + + Context "Happy Path" { + + Mock -ModuleName $moduleForMock -CommandName Get-SupportedPlatformAPMVersionMap -MockWith { + + return @( + @{ + APMVersion = "8.6.7.5309"; + PlatformMinimumVersion = "0.0.0.0"; + PlatformMaximumVersion = "2.0.0.0"; + }, + @{ + APMVersion = "192.168.1.1"; + PlatformMinimumVersion = "2.0.0.1"; + PlatformMaximumVersion = "2.9.9.9"; + }, + @{ + APMVersion = "8.8.8.8"; + PlatformMinimumVersion = "3.0.0.0"; + PlatformMaximumVersion = $null; + } + ) + } + + $happyPathTestCases = @( + @{ version = "0.0.0.0"; apmVersion = "8.6.7.5309"; }, + @{ version = "0.0.0.1"; apmVersion = "8.6.7.5309"; }, + @{ version = "1.0.0.0"; apmVersion = "8.6.7.5309"; }, + @{ version = "1.1.1.1"; apmVersion = "8.6.7.5309"; }, + @{ version = "2.0.0.0"; apmVersion = "8.6.7.5309"; }, + @{ version = "2.0.0.1"; apmVersion = "192.168.1.1"; }, + @{ version = "2.1.3.4"; apmVersion = "192.168.1.1"; }, + @{ version = "2.9.9.9"; apmVersion = "192.168.1.1"; }, + @{ version = "3.0.0.0"; apmVersion = "8.8.8.8"; }, + @{ version = "99.99.99.99"; apmVersion = "8.8.8.8"; } + ) + + It "Returns Expected Values: ()" -TestCases $happyPathTestCases { + + param ($version, $apmVersion) + Get-SupportedPlatformAPMVersion -PlatformVersion $version | Should -Be $apmVersion + } + + $zeroPaddingTestCases = @( + @{ version = "0.0"; apmVersion = "8.6.7.5309"; }, + @{ version = "0.0.0"; apmVersion = "8.6.7.5309"; }, + @{ version = "2.1"; apmVersion = "192.168.1.1"; }, + @{ version = "2.1.0"; apmVersion = "192.168.1.1"; } + ) + + It "Pads Short Versions and Returns Expected Values: ()" -TestCases $zeroPaddingTestCases { + + param ($version, $apmVersion) + Get-SupportedPlatformAPMVersion -PlatformVersion $version | Should -Be $apmVersion + } + } + + Context "Parameter Handling" { + + It "Does Not Attempt Remote Connection if Platform Version Provided" { + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith {} + + Get-SupportedPlatformAPMVersion -PlatformVersion "1.2.3.4" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 0 -Exactly -Scope It + } + + It "Looks Up the Local ORB Version if No Parameters Provided" { + + Mock -ModuleName $moduleForMock -CommandName Invoke-Command -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-ORBVersion -MockWith { return "1.1.1.1" } + + Get-SupportedPlatformAPMVersion + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ORBVersion -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-Command -Times 0 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.ps1 new file mode 100644 index 0000000..3ca2c95 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.ps1 @@ -0,0 +1,41 @@ +function Get-SupportedPlatformAPMVersionMap { + +<# +.SYNOPSIS + Returns a Hashtable Array Mapping NewRelic APM Version to Alkami Platform Version + +.DESCRIPTION + Returns a Hashtable Array Mapping NewRelic APM Version to Alkami Platform Version. Only value is for mocking, and for clarity + when future changes are required. An array maxmimum value of $null will set it as the current version, for deploy and bootstrapping + purposes. + +.Example + Get-SupportedPlatformAPMVersionMap + + Name Value + ---- ----- + PlatformMaximumVersion + APMVersion 8.39.0.0 + PlatformMinimumVersion 0.0.0.0 + +.LINK + https://confluence.alkami.com/x/En5FCw +#> + + [CmdletBinding()] + [OutputType([PSCustomObject[]])] + param() + + return @( + @{ + APMVersion = "8.39.0.0"; + PlatformMinimumVersion = "0.0.0.0"; + PlatformMaximumVersion = "2022.4.0.9999"; + } + @{ + APMVersion = "10.3.0.0"; + PlatformMinimumVersion = "2022.5.0.0"; + PlatformMaximumVersion = $null; + } + ) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.tests.ps1 new file mode 100644 index 0000000..693c22c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-SupportedPlatformAPMVersionMap.tests.ps1 @@ -0,0 +1,51 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +############################ +# These Are Not Unit Tests # +############################ + +# These just attempt to ensure nobody accidentally mangles the hashtable array returned by the function +# which would result in hilarity, but also a bunch of bad things too + +Describe "Get-SupportedPlatformAPMVersionMap" { + + Context "Poor Life Choices" { + + It "Has One and Only One Element With a Null PlatformMaximumVersion" { + + # Did this test fail? You accidentally have no elements or more than 1 elements in the APM <-> Platform Version Map with a Null Max Value. + # + # There can be only one. + # - Connor MacLeod + $nullMaxVersions = Get-SupportedPlatformAPMVersionMap | Where-Object { $null -eq $_.PlatformMaximumVersion } + ([hashtable[]]$nullMaxVersions).Count | Should -Be 1 + } + + It "Only Has One Element With a 0.0.0 PlatformMinimumVersion" { + + # Did this test fail? You accidentally have no elements or more than 1 elements in the APM <-> Platform Version Map with a Min Value Equivilant to 0 + # + # One is the lonliest number that you'll ever do + # - Three Dog Night + $zeroMinVersions = Get-SupportedPlatformAPMVersionMap | Where-Object { ("0.0.0" -eq $_.PlatformMinimumVersion) -or ("0.0.0.0" -eq $_.PlatformMinimumVersion) } + ([hashtable[]]$zeroMinVersions).Count | Should -Be 1 + } + + It "Doesn't Have Any Duplicate APM Version Entries" { + + # Did this test fail? You accidentally have more than one element with the same APM version. That shouldn't be needed. Discuss with the author if you think I'm wrong! + # + # You cannot duplicate this, you can't do me, I'm complicated + # -Lil Nacho + $map = Get-SupportedPlatformAPMVersionMap + $groupedValues = $map | ForEach-Object { $_.APMVersion } | Group-Object + $groupedValues | Where-Object {$_.Count -gt 1} | Select-Object -ExpandProperty Name | Should -BeNullOrEmpty + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-TempOrbDeployPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-TempOrbDeployPath.ps1 new file mode 100644 index 0000000..c6d8717 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-TempOrbDeployPath.ps1 @@ -0,0 +1,11 @@ +function Get-TempOrbDeployPath { +<# +.SYNOPSIS + Get the path to the default deploy location where the legacy Orb WCF/Client folders +#> + [CmdletBinding()] + Param() + + return (Join-Path (Join-Path (Join-Path $env:SystemDrive "temp") "deploy") "Orb") +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-TextEditorPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-TextEditorPath.ps1 new file mode 100644 index 0000000..9fcbed2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-TextEditorPath.ps1 @@ -0,0 +1,37 @@ +function Get-TextEditorPath { +<# +.SYNOPSIS + Get the path to a text editor executable. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + $logLead = Get-LogLeadName + + # Read the default .txt editor from the registry. + $defaultEditor = (Get-ItemProperty -Path 'Registry::HKEY_CLASSES_ROOT\txtfile\shell\open\command').'(Default)'; + + # Strip off the placeholder %1's after the .exe in the default application string. Ex "c:\Program Files\notepad++\notepad++.exe %1" + $exeIndex = $defaultEditor.IndexOf(".exe",[StringComparison]::CurrentCultureIgnoreCase); + if($exeIndex -lt 0) + { + throw "$logLead : Default text editor `"$defaultEditor`" is not an executable. What's up with that?" + } + $defaultEditor = $defaultEditor.Substring(0, $exeIndex + 4); + + # If the default text editor is notepad, see if notepad++ exists instead. + $nppPath = 'C:\Program Files\Notepad++\notepad++.exe' + if(($defaultEditor -like "*\NOTEPAD.EXE*") -and (Test-Path $nppPath)) + { + return $nppPath; + } + + # Otherwise just use the default. Hopefully it's not notepad. + if(!(Test-Path $defaultEditor)) + { + throw "$logLead : The default text editor path is invalid!" + } + + return $defaultEditor; +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.ps1 new file mode 100644 index 0000000..603bf5a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.ps1 @@ -0,0 +1,47 @@ +function Get-UncPath { +<# +.SYNOPSIS + Returns the UNC path of a given filename to a remote machine. +.PARAMETER filePath + The full file path of the file. +.PARAMETER ComputerName + The computer name of the remote machine. +.PARAMETER IgnoreLocalPaths + Switch param to return the $filePath if the ComputerName is the local machine. +#> + param( + [Parameter(Mandatory = $true)] + [Alias("Path")] + [string]$filePath, + + [Parameter(Mandatory = $true)] + [string]$ComputerName, + + [Parameter(Mandatory = $false)] + [switch]$IgnoreLocalPaths + ) + + $logLead = (Get-LogLeadName); + + Write-Verbose "$logLead Constructing UNC path for `"$filepath`" on remote host $ComputerName"; + + # Return the local path if UNC pathing isn't required. + if($IgnoreLocalPaths.IsPresent) { + if(($ComputerName -eq "localhost") -or ($ComputerName -eq $env:COMPUTERNAME) -or ($ComputerName -eq (Get-FullyQualifiedServerName))) { + return $filePath + } + } + + # Get the drive letter, and strip it out of the filepath. + $driveLetter = (Split-Path -Path $filePath -Qualifier); + $filePath = $filePath.Substring($driveLetter.Length); + $driveLetter = $driveLetter.Replace(":",""); + + # Build UNC path. + $baseUNC = "\\$ComputerName\$driveLetter`$"; + $filePath = (Join-Path $baseUNC $filePath); + + Write-Verbose "$logLead Constructed UNC path `"$filePath`""; + + return $filePath; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.tests.ps1 new file mode 100644 index 0000000..bb1d089 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-UncPath.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-UncPath" { + + Context "Returns Correct Values" { + + It "Should Have Correct C:/ Drive UNC Directory Path" { + + $result = Get-UncPath -filePath "C:/temp/test/path" -ComputerName "FAKE1234" + $result | should -BeExactly "\\FAKE1234\C$\temp\test\path" + } + + It "Should Have Correct D:/ Drive UNC Directory Path" { + + $result = Get-UncPath -filePath "D:/temp/test/path" -ComputerName "FAKE1234" + $result | should -BeExactly "\\FAKE1234\D$\temp\test\path" + } + + It "Should Have Correct C:/ Drive UNC File Path" { + + $result = Get-UncPath -filePath "C:/temp/test/path/file.txt" -ComputerName "FAKE1234" + $result | should -BeExactly "\\FAKE1234\C$\temp\test\path\file.txt" + } + + It "Should Have Correct D:/ Drive UNC File Path" { + + $result = Get-UncPath -filePath "D:/temp/test/path/file.txt" -ComputerName "FAKE1234" + $result | should -BeExactly "\\FAKE1234\D$\temp\test\path\file.txt" + } + } + + Context "Handles Local UNC Pathing Ignore Flag" { + + It "Should Have UNC Path to Local Machine Without Flag" { + $result = Get-UncPath -filePath "D:/temp/test/path/file.txt" -ComputerName ($env:COMPUTERNAME) + $result | should -BeExactly "\\$env:COMPUTERNAME\D$\temp\test\path\file.txt" + } + + It "Should Have Correct localhost Path" { + $filepath = "C:/temp/test/path/file.txt"; + $result = Get-UncPath -filePath $filepath -ComputerName "localhost" -IgnoreLocalPaths + $result | should -BeExactly $filepath; + } + + It "Should Have Correct ComputerName Path" { + $filepath = "C:/temp/test/path/file.txt"; + $result = Get-UncPath -filePath $filepath -ComputerName ($env:COMPUTERNAME) -IgnoreLocalPaths + $result | should -BeExactly $filepath; + } + + It "Should Have Correct FQDN Path" { + $filepath = "C:/temp/test/path/file.txt"; + $result = Get-UncPath -filePath $filepath -ComputerName (Get-FullyQualifiedServerName) -IgnoreLocalPaths + $result | should -BeExactly $filepath; + } + + It "Should Return UNC Path for Non-Local Machine" { + $filepath = "C:/temp/test/path/file.txt"; + $result = Get-UncPath -filePath $filepath -ComputerName "FAKE1234" -IgnoreLocalPaths + $result | should -BeExactly "\\FAKE1234\C$\temp\test\path\file.txt"; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-UsernameFromSid.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-UsernameFromSid.ps1 new file mode 100644 index 0000000..ff67f5a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-UsernameFromSid.ps1 @@ -0,0 +1,21 @@ +function Get-UsernameFromSid { + <# +.SYNOPSIS + Converts a SID to a Username + +.PARAMETER SecurityIdentifier + Identifier to use to find a username +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("SID")] + [string]$SecurityIdentifier + ) + + $objSID = New-Object System.Security.Principal.SecurityIdentifier($SecurityIdentifier) + $objUser = $objSID.Translate([System.Security.Principal.NTAccount]) + return $objUser.Value +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-UsersPath.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-UsersPath.ps1 new file mode 100644 index 0000000..813724e --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-UsersPath.ps1 @@ -0,0 +1,24 @@ +function Get-UsersPath { +<# +.SYNOPSIS + Get the path to the root of the Users folder for Windows or macOS. + Will optionally append the provided username path as well, when provided. + +.PARAMETER Username + [Optional] The username folder to append. This is provided as a helper parameter. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$Username = "" + ) + + $systemDrive = $env:SystemDrive + if ([string]::IsNullOrWhiteSpace($systemDrive)) { + $systemDrive = (Get-Item -Path $PSScriptRoot).PSDrive.Root + } + + # (Join-Path C:\ "") -> C:\ + # (Join-Path C:\ $null) -> C:\ + return (Join-Path (Join-Path $systemDrive "Users") $Username); +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-ValidatedRuntimeParameter.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-ValidatedRuntimeParameter.ps1 new file mode 100644 index 0000000..0a7af11 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-ValidatedRuntimeParameter.ps1 @@ -0,0 +1,31 @@ +function Get-ValidatedRuntimeParameter { +<# +.SYNOPSIS + Allows for validating runtime parameter to ensure consistency. + +.PARAMETER Runtime + Appropriate values are core, dotnetcore, framework, legacy + +.OUTPUTS + dotnetcore, framework, as appropriate + +.NOTES + Used in Alkami.Installer.Service, Alkami.Installer.Migrations to validate the runtime value + Used in Get-PackageMetadataV2 to validate the runtime value +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Runtime + ) + + $logLead = Get-LogLeadName + + switch ($Runtime) { + { "framework","legacy" -eq $_ } { return "framework" } + { "dotnetcore","core" -eq $_ } { return "dotnetcore" } + default { throw "$logLead : [$_] is not allowed as a valid runtime. Please consult with SRE for adding additional runtimes." } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Get-VersionPSObject.ps1 b/Modules/Alkami.PowerShell.Common/Public/Get-VersionPSObject.ps1 new file mode 100644 index 0000000..c497373 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Get-VersionPSObject.ps1 @@ -0,0 +1,42 @@ +function Get-VersionPSObject { +<# +.SYNOPSIS + Parse a function into a semver compatible version object instead of the naive .NET implementation. + The .NET implementation existed before the semver formalization, so it does not implement all of the same properties/metadata concepts. + While a third-party library may exist that neatly handles these edge-cases, we do not use those in PowerShell due to availability concerns. + +.PARAMETER Version + [string] The value to parse. +#> + [CmdletBinding()] + [OutputType([object])] + param( + [Parameter(Mandatory=$true)] + [string]$Version + ) + +# region Semversion variables +$AllowFourPartsVersion = "(?\d+(\s*\.\s*\d+){0,3})"; + +# For some reason Chocolatey version uses "-" instead of "+" for the build metadata. Here change it to "-" +$ReleasePatternDash = "(?-[A-Z0-9a-z]+(\.[A-Z0-9a-z]+)*)?"; +$BuildPatternDash = "(?\-[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; + +# we use this one because Chocolatey uses -- format +$SemanticVersionPatternDash = "^" + $AllowFourPartsVersion + $ReleasePatternDash + $BuildPatternDash + "$" + + $isMatch=$Version.Trim() -match $SemanticVersionPatternDash + if( $isMatch ) { + if ($Matches.Version) {$v = $Matches.Version.Trim()} else {$v = $Matches.Version} + if ($Matches.Release) {$r = $Matches.Release.Trim("-, +")} else {$r = $Matches.Release} + if ($Matches.Build) {$b = $Matches.Build.Trim("-, +")} else {$b = $Matches.Build} + + return New-Object PSObject -Property @{ + Version = $v + Release = $r + Build = $b + } + } else { + Write-Error "Could not determine semantic version of $Version" + } + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Grant-RightsToFolderOrFile.ps1 b/Modules/Alkami.PowerShell.Common/Public/Grant-RightsToFolderOrFile.ps1 new file mode 100644 index 0000000..c590a38 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Grant-RightsToFolderOrFile.ps1 @@ -0,0 +1,32 @@ +function Grant-RightsToFolderOrFile { +<# +.SYNOPSIS + Sets ACL on a folder or file for a user +#> + param ( + [string]$Account, + [string]$Path, + [System.Security.AccessControl.FileSystemRights]$Rights + ) + + $logLead = (Get-LogLeadName); + + if ((Get-Item $Path).PSIsContainer) { + $newRights = New-Object System.Security.AccessControl.FileSystemAccessRule($Account, $Rights, "ContainerInherit,ObjectInherit", "None", "Allow") + } + else { + $newRights = New-Object System.Security.AccessControl.FileSystemAccessRule($Account, $Rights, "Allow") + } + + $acl = Get-Acl $Path + $existingPermissions = $acl.Access | Where-Object {$_.IdentityReference.Value -like ("*{0}" -f $Account)} + + if (($existingPermissions | Where-Object {$_.FileSystemRights -like ("*{0}*" -f $Rights)}).Count -gt 0) { + Write-Output ("$logLead : Account or group {0} already has the specified rights to {1}" -f $Account, $Path) + return + } + + $acl.SetAccessRule($newRights) + Set-Acl $Path $acl +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Grant-UserLocalSecurityPolicyRights.ps1 b/Modules/Alkami.PowerShell.Common/Public/Grant-UserLocalSecurityPolicyRights.ps1 new file mode 100644 index 0000000..ba85c0a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Grant-UserLocalSecurityPolicyRights.ps1 @@ -0,0 +1,58 @@ +function Grant-UserLocalSecurityPolicyRights { +<# +.SYNOPSIS + Grants a User the Specified Right in the Local Security Policy +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$userName, + + [Parameter(Mandatory = $true)] + [string]$policyName + ) + + $logLead = (Get-LogLeadName); + $userSid = Get-SidFromUsername $username + + if ([String]::IsNullOrEmpty($userSid)) { + Write-Warning ("$logLead : Could not find the SID for username {0}" -f $userName) + return + } + + Write-Verbose ("$logLead : SID for Supplied Username is {0}" -f $userSid) + + Write-Output ("$logLead : Getting current security policy setting for policy {0}" -f $policyName) + $currentValue = Get-SecurityPolicySetting $policyName + + if ($currentValue -like "*$($userSid)*") { + Write-Output ("$logLead : The specified user {0} already has the right {1} on this machine" -f $userName, $policyName) + return + } + + if ([String]::IsNullOrEmpty($currentValue)) { + Write-Warning ("$logLead : Could not parse the current {0} value. Breaking function to avoid breaking system." -f $policyName) + return + } + + $newSetting = ("{0},*{1}" -f $currentValue, $userSid) + + $newSecurityContent = @" +[Unicode] +Unicode=yes +[Version] +signature="`$CHICAGO`$" +Revision=1 +[Privilege Rights] +$($policyName) = $($newSetting) +"@ + + $importFile = [System.IO.Path]::GetTempFileName() + Write-Verbose ("$logLead : Saving modified security file to {0}" -f $importFile) + $newSecurityContent | Set-Content -Path $importFile -Encoding Unicode -Force + + Write-Output ("$logLead : Importing Modified Security Policy") + secedit.exe /configure /db "secedit.sdb" /cfg "$($importFile)" /areas USER_RIGHTS +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Grant-UserProfileSystemPerformanceRights.ps1 b/Modules/Alkami.PowerShell.Common/Public/Grant-UserProfileSystemPerformanceRights.ps1 new file mode 100644 index 0000000..ac6c139 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Grant-UserProfileSystemPerformanceRights.ps1 @@ -0,0 +1,19 @@ +function Grant-UserProfileSystemPerformanceRights { +<# +.SYNOPSIS + Grants a User the Profile System Performance Right +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$userName + ) + + $logLead = (Get-LogLeadName); + Grant-UserLocalSecurityPolicyRights $userName "SeSystemProfilePrivilege" + + $secPolLogContent = Get-Content (Join-Path $env:windir "security\logs\scesrv.log") + $secPolLogContent | ForEach-Object { Write-Verbose ("$logLead : {0}" -f $_)} +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Import-AWSModule.ps1 b/Modules/Alkami.PowerShell.Common/Public/Import-AWSModule.ps1 new file mode 100644 index 0000000..47276dd --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Import-AWSModule.ps1 @@ -0,0 +1,24 @@ +function Import-AWSModule { +<# +.SYNOPSIS + Load the AWS module in a common way, only load it if it hasn't been loaded yet +#> + [CmdletBinding()] + param ( + + ) + + if ($null -eq (Get-Module AWSPowershell)) { + try { + (Import-Module AWSPowershell -Scope Global) | Out-Null + } catch { + $modulePath = "C:\Program Files (x86)\AWS Tools\PowerShell\awspowershell" + if (!(Test-Path $modulePath)) { + Write-Error "Could not find the path for the AWSPowershell module, expected to find it at [$modulePath]" + } else { + Write-Error "Could not import AWSPowershell module" + Write-Error $_.Exception.Message + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Import-TeamCityModule.ps1 b/Modules/Alkami.PowerShell.Common/Public/Import-TeamCityModule.ps1 new file mode 100644 index 0000000..5a256b5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Import-TeamCityModule.ps1 @@ -0,0 +1,19 @@ +function Import-TeamCityModule { + <# +.SYNOPSIS + Load the TeamCity module in a common way, only load it if it hasn't been loaded yet +#> + [CmdletBinding()] + param ( + + ) + + if ($null -eq (Get-Module Alkami.DevOps.TeamCity)) { + try { + (Import-Module Alkami.DevOps.TeamCity -Scope Global) | Out-Null + } catch { + Write-Warning "Could not import AWSPowershell module" + Write-Warning $_.Exception.Message + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-CallOperatorWithPathAndParameters.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-CallOperatorWithPathAndParameters.ps1 new file mode 100644 index 0000000..3fbeb4c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-CallOperatorWithPathAndParameters.ps1 @@ -0,0 +1,29 @@ +function Invoke-CallOperatorWithPathAndParameters { +<# +.SYNOPSIS + This function is basically just a wrapper for unit-testing purposes. + Used to call a system file with an array of string args. + The best use-case for this is sc.exe execution, or topshelf installers. + +.PARAMETER Path + The file path being invoked + +.PARAMETER Arguments + This value just gets splatted as passed in. + +.OUTPUTS + This function outputs whatever came out of the called function +#> + [CmdletBinding()] + [OutputType([System.Object])] + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [Parameter(Mandatory=$true)] + [string[]]$Arguments + ) + + # Fun fact: $LASTEXITCODE is set by the past .exe to run in the call stack, not by functions + # So if this sets a $LASTEXITCODE we can test for it in the caller function + (& $Path @Arguments) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.ps1 new file mode 100644 index 0000000..a14a21a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.ps1 @@ -0,0 +1,114 @@ +function Invoke-CommandWithRetry { +<# +.SYNOPSIS + Executes a script block and retries the operation a given number of times if it fails. + Executions are applied by either linear or exponential format and always contain an amount of random jitter in milliseconds. + The jitter range can be managed by appropriate flags. Defaults are 50ms to 500ms. + +.EXAMPLE + $result = Invoke-CommandWithRetry -ScriptBlock $script -MaxRetries 3 -SecondsDelay 1 -Exponential + +.PARAMETER ScriptBlock + The script block to execute + +.PARAMETER Arguments + An array of objects to pass into the scriptblock + +.PARAMETER MaxRetries + The maxumim number of retries to attempt. Defaults to 5. + +.PARAMETER Seconds + The number of seconds to delay for each retry. Defaults to 1 when used. + +.PARAMETER Milliseconds + The number of milliseconds to delay for each retry. Defaults to 1000 ms. + +.PARAMETER Exponential + [switch] Exponentially increment the time delay by a power of 2 for each attempt + The alternative to exponential is linear. + +.PARAMETER JitterMin + [int] The min number of milliseconds to jitter by + +.PARAMETER JitterMax + [int] The max number of milliseconds to jitter by +#> + [CmdletBinding(DefaultParameterSetName='Milliseconds')] + [OutputType([System.Object])] + Param( + [Parameter(Mandatory=$true)] + [scriptblock]$ScriptBlock, + + [Parameter(Mandatory=$false)] + [object[]]$Arguments = $null, + + [Parameter(Mandatory=$false)] + [int]$MaxRetries = 5, + + [Parameter(ParameterSetName='Seconds', Mandatory=$false)] + [Alias('SecondsDelay')] + [int]$Seconds = 1, + + [Parameter(ParameterSetName='Milliseconds', Mandatory=$false)] + [int]$Milliseconds = 1000, + + [Parameter(Mandatory=$false)] + [switch]$Exponential, + + [Parameter(Mandatory=$false)] + [int]$JitterMin = -500, + + [Parameter(Mandatory=$false)] + [int]$JitterMax = 500 + ) + + $logLead = (Get-LogLeadName) + $retryCount = 0 + + if ($PSCmdlet.ParameterSetName -eq 'Seconds') { + $Milliseconds = $Seconds * 1000 + } + + do { + # Not being picked up by above call + $logLead = "[Invoke-CommandWithRetry]" + $retryCount++ + try { + $ScriptBlock.Invoke($Arguments) + return + } catch { + Write-Warning $_.Exception.InnerException.Message + + # Don't stall at the end of the failed run otherwise we delay error reporting by one more sleep cycle + if ($retryCount -lt $MaxRetries) { + # As a deconstructed equation because the below block covers a lot of ground + # $sleepTimeout = ($milliseconds * (1 OR a power of 2) ) + random jitter amount + # 1 -shl 0 = 1 1 -shl 3 = 8 3rd retry = 8 multiplier + # $sleepTimeout = ($milliseconds * (1 -shl (0,($retryCount - 1))[$Exponential])) + (Get-Random -Maximum $JitterMax -Minimum $JitterMin) + # $sleepTimeoutModifier + # -shiftleft + # $retryCount - 1 when exponential, 1 when linear + # $sleepTimeout = ($milliseconds * $sleepTimeoutModifier ) + $jitterAmount + + $jitterAmount = Get-Random -Maximum $JitterMax -Minimum $JitterMin + $sleepTimeoutModifier = 1 + if ($Exponential) { + # When you want 2 to the power, you just shift the binary leader left + # 1 = 0001 + # 2 = 0010 + # 4 = 0100 + # 8 = 1000 + $sleepTimeoutModifier = $sleepTimeoutModifier -shl ($retryCount - 1) #decremented because we start at 1 retry count + } + # 3rd retry would be base timeout * 8 + jitter + $sleepTimeout = ($milliseconds * $sleepTimeoutModifier) + $jitterAmount + Write-Warning "$logLead : Attempt #$RetryCount failed. Start-Sleep -Milliseconds [$sleepTimeout] for retry" + Start-Sleep -Milliseconds $sleepTimeout + } else { + Write-Warning "$logLead : Attempt #$RetryCount failed." + } + } + } while ($retryCount -lt $MaxRetries) + + Write-Error "$logLead : Execution failed. Maximum retries attempted." +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.tests.ps1 new file mode 100644 index 0000000..a0c9272 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.tests.ps1 @@ -0,0 +1,76 @@ +#. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Invoke-CommandWithRetry" { + + Context "Test linear error-outs" { + Mock -ModuleName $moduleForMock -CommandName Get-Random -MockWith { return 0 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + + $global:WarningStatements = @() + $global:WarningStatementCount = 0 + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {param($message) if ($message -match "Attempt #") { + $messageSplit = (($message -split '\[')[2] -split ']')[0] + if (![string]::IsNullOrWhiteSpace($messageSplit)) { + $global:WarningStatements += "$messageSplit" + } + $global:WarningStatementCount++ + }} + + $scriptDelayMilliseconds = 200 + + $maxRetries = 3; + + $array = (1..($maxRetries - 1))|%{"$scriptDelayMilliseconds"} + + Invoke-CommandWithRetry -Milliseconds $scriptDelayMilliseconds -ScriptBlock { throw 'test' } -MaxRetries $maxRetries + + It "should have the right elements in the array" { + $global:WarningStatements | Should -Be $array + } + + It "should have done `$maxRetries count" { + $global:WarningStatementCount | Should -Be $maxRetries + } + } + + Context "Exponential error-outs" { + Mock -ModuleName $moduleForMock -CommandName Get-Random -MockWith { return 0 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + + $global:WarningStatements = @() + $global:WarningStatementCount = 0 + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {param($message) if ($message -match "Attempt #") { + $messageSplit = (($message -split '\[')[2] -split ']')[0] + if (![string]::IsNullOrWhiteSpace($messageSplit)) { + $global:WarningStatements += "$messageSplit" + } + $global:WarningStatementCount++ + }} + + $scriptDelayMilliseconds = 200 + + $maxRetries = 3; + + $array = (1..($maxRetries - 1))|%{"$($scriptDelayMilliseconds * (1 -shl ($_ - 1)))"} + + Invoke-CommandWithRetry -Milliseconds $scriptDelayMilliseconds -ScriptBlock { throw 'test' } -MaxRetries $maxRetries -Exponential + + It "should have the right elements in the array" { + $global:WarningStatements | Should -Be $array + } + + It "should have done `$maxRetries count" { + $global:WarningStatementCount | Should -Be $maxRetries + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.ps1 new file mode 100644 index 0000000..e319daf --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.ps1 @@ -0,0 +1,346 @@ +function Invoke-Parallel { + <# + .SYNOPSIS + Executes a script block against a list of objects in parallel with PSJobs. + .PARAMETER Objects + Objects to operate against, in parallel + .PARAMETER Script + The ScriptBlock to execute + .PARAMETER Arguments + Arguments (or parameters) to pass to the script block, "globally" so that each Object is operated on "equally" + .PARAMETER ReturnObjects + Whether to collect the return values from your Script and return it in an array at the end. It is YOUR responsibility + to craft a ScriptBlock that returns data or return values that YOU can use. + .PARAMETER ThreadPerObject + Each object gets its own thread, instead of batched operation. Batched operation can save time in PSSession spin up and tear down. + The more objects, the more significant this can be. + .PARAMETER InitializationScript + This is the same as Invoke-Command's InitializationScript parameter. Use it to prepare the session with parameters or functions that you need + .PARAMETER ContinueOnFailure + Whether to Stop on Errors when Receiving the Jobs that executed your Script. Defaults to $True. If you set this to $False, the + first error in a Script will cause the rest of the Objects to not have the Script run against them. Be careful with this one. + .PARAMETER CleanupJobs + Whether to explicitly call Remove-Job when calling Receive-Job as an attempt to manually manage memory + .NOTES + The arguments are passed to each job globally. If you want to pass different arguments to different jobs, format it into the object[] objects argument. + Use -returnObjects if you want the results of the jobs read back, ala with a return statement. Just be careful with Write-Output usage. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object[]]$Objects, + [Parameter(Mandatory = $true)] + [object]$Script, + [Parameter(Mandatory = $false)] + [object[]]$Arguments = $null, + [Parameter(Mandatory = $false)] + [int]$NumThreads = 8, + [Parameter(Mandatory = $false)] + [switch]$ReturnObjects, + [Parameter(Mandatory = $false)] + [switch]$ThreadPerObject, + [Parameter(Mandatory = $false)] + [ScriptBlock]$InitializationScript = $null, + [Parameter(Mandatory = $false)] + [bool]$ContinueOnFailure = $true, + [Parameter(Mandatory = $false)] + [switch]$CleanupJobs + ) + process { + $loglead = Get-LogLeadName + # Return if there are no elements to process. + if (Test-IsCollectionNullOrEmpty $objects) { + if ($returnObjects) { + return $null + } else { + return + } + } + + # These are all the states that are "done" as in not doing anything + # I think "Suspended" should only be for Workflows + # I think "Disconnected" should only be for PSRemoting sessions + # those will come into play for Invoke-ParallelServers + # and anywhere we use PSSessions explicitly or Invoke-Command -AsJob + # + # Why these? https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/wait-job?view=powershell-5.1#description + # {quote} + # The Wait-Job cmdlet waits for a job to be in a terminating state before continuing execution. The terminating states are: + # + # Completed + # Failed + # Stopped + # Suspended + # Disconnected + # You can wait until a specified job, or all jobs are in a terminating state. + # You can also set a maximum wait time for the job using the Timeout parameter, or use the Force parameter to wait for a job in the Suspended or Disconnected states. + # {quote} + # + # really, I'd rather put Suspended and Disconnected somewhere else and handle them differently. But I'm not sure how. + # Until I do, I'll follow the documentation. + # Not that we can do anything with Suspended and Disconnected... + + $terminatingJobStates = @( + "Completed", + "Failed", + "Stopped", + "Suspended", + "Disconnected" + ) + + + # These are all the states that are not "done" + # some are close to done, but I am not sure they can be counted + # how long does it take to get from Stopping to Stopped? + # I don't know either + $runningJobStates = @( + "Running", + "NotStarted", + "Stopping", + "Suspending" + ) + + # Wait! Where did these things come from? They weren't in the other link! + # Good catch. https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.jobstate?view=powershellsdk-1.1.0#fields + # + # I really don't know what to do with these, but they exist, and can show up. + # We shall start with "log and hope" + $wonkyJobStates = @( + "AtBreakpoint", + "Blocked" + ) + + # Cap the number of threads. + $objectCount = $Objects.Count + # I know this is non-standard. It pre-dates our standards. If I have time in this story, I'll try to fix it. TR - 20221012 + if ($NumThreads -gt $objectCount) { + $NumThreads = $objectCount + } + + # Figure out what to do when something fails. + $errorAction = if ($ContinueOnFailure) { "Continue" } else { "Stop" } + + [array]$jobs = @() + [array]$results = @() + [array]$wonkyJobs = @() + [array]$completedJobs = @() + + # For each input object. + if ($ThreadPerObject) { + #region OneObjectPerJob + # Create a PSJob for each object. + foreach ($object in $Objects) { + # Wait for any job to complete if there are any. + # Also, jobs that end up "Blocked" will throw, here... + # If we have hit the max number of concurrent jobs, wait. + if ( -NOT (Test-IsCollectionNullOrEmpty -Collection $jobs) -and ($jobs.Count -ge $NumThreads)) { + Write-Verbose "$loglead : maximum jobs running... wating for any job to complete..." + Wait-Job -Job $jobs -Any | Out-Null + } + + # CHECK JOB STATES + # Scrub the jobs array of jobs that have finished, and receive their outputs. + # $runningJobs = $jobs | Where-Object { $_.State -in $runningJobStates }; + # $completedJobs = $jobs | Where-Object { $_.State -in $terminatingJobStates } + $completedJobs = $jobs.Where({ + $_.State -in $terminatingJobStates + }) + $completedJobIds = $completedJobs.Id + $wonkyJobs = $jobs.Where({ + $_.State -in $wonkyJobStates + }) + $wonkyJobIds = $wonkyJobs.Id + + if (!(Test-IsCollectionNullOrEmpty $completedJobs)) { + + foreach ($completedJob in $completedJobs) { + $jobName = $completedJob.Name + $jobState = $completedJob.State + Write-Verbose "Receiving job named for object $jobName in state $jobState" + if ($ReturnObjects) { + $results += Receive-Job -Job $completedJob -ErrorAction $errorAction + + } else { + Receive-Job -Job $completedJob -ErrorAction $errorAction + + } + if ($CleanupJobs) { + Write-Verbose "Removing job named for object $jobName" + Remove-Job -Job $completedJob -ErrorAction SilentlyContinue + } + Write-Verbose "Done receiving job named for object $jobName in state $jobState" + + } + } + + if (-NOT (Test-IsCollectionNullOrEmpty -Collection $wonkyJobs)) { + # Stop and Remove WONKY jobs + # where "wonky" is in the list above + foreach ($wonkyJob in $wonkyJobs) { + $jobName = $wonkyJob.Name + $jobState = $wonkyJob.State + Write-Warning "$loglead : Job named for object $jobName was in state $jobState - this is not recoverable" + Write-Warning "$loglead : Job data will be printed, job will be stopped, then removed. ErrorAction Continue is being forced." + Write-Warning "$loglead : We're all fine down here. How are you? ... Luke! We're gonna have company!" + Format-List -InputObject $wonkyJob -Property * -Force + Stop-Job -Job $wonkyJob -ErrorAction Continue + Remove-Job -Job $wonkyJob -ErrorAction Continue + Write-Warning "$loblead : Done Stopping and Removing job named for object $jobName" + + } + } + + # Repopulate the jobs array without Completed and Wonky jobs that have been Received and Removed, respectively + [array]$jobs = $jobs.Where({ + $_.Id -notin $completedJobIds -and + $_.Id -notin $wonkyJobIds + }) + + # Start a new job. + if (Test-StringIsNullOrWhitespace -Value $object.Name) { + if ($object.GetType().Name -eq "String") { + $objectName = $object + } else { + $objectName = $null + } + } else { + $objectName = $object.Name + } + Write-Verbose "Starting job for object $objectName" + $jobs += Start-Job -Name $objectName -ScriptBlock $Script -ArgumentList $object, $Arguments -InitializationScript $InitializationScript + } + + # Another round of Wonky Job cleanup + $wonkyJobs = $jobs.Where({ + $_.State -in $wonkyJobStates + }) + $wonkyJobIds = $wonkyJobs.Id + + if (-NOT (Test-IsCollectionNullOrEmpty -Collection $wonkyJobs)) { + # Stop and Remove WONKY jobs + # where "wonky" is in the list above + foreach ($wonkyJob in $wonkyJobs) { + $jobName = $wonkyJob.Name + $jobState = $wonkyJob.State + Write-Warning "$loglead : Job named for object $jobName was in state $jobState - this is not recoverable" + Write-Warning "$loglead : Job data will be printed, job will be stopped, then removed. ErrorAction Continue is being forced." + Write-Warning "$loglead : We're all fine down here. How are you? ... Luke! We're gonna have company!" + Format-List -InputObject $wonkyJob -Property * -Force + Stop-Job -Job $wonkyJob -ErrorAction Continue + Remove-Job -Job $wonkyJob -ErrorAction Continue + Write-Warning "$loblead : Done Stopping and Removing job named for object $jobName" + + } + } + + # Repopulate the jobs array without Wonky jobs that have been Removed + [array]$jobs = $jobs.Where({ + $_.Id -notin $wonkyJobIds + }) + + # Wait for all outstanding jobs to complete. + Write-Verbose "Waiting for jobs to complete..." + Wait-Job -Job $jobs | Out-Null + + # If we want to return the output stream from jobs in a list. + foreach ($job in $jobs) { + $jobName = $job.Name + $jobState = $job.State + Write-Verbose "Receiving job named for object $jobName in state $jobState" + if ($ReturnObjects) { + $results += Receive-Job -Job $job -ErrorAction $errorAction + + } else { + Receive-Job -Job $job -ErrorAction $errorAction + + } + + if ($CleanupJobs) { + Write-Verbose "Removing job named for object $jobName" + Remove-Job -Job $job -ErrorAction SilentlyContinue + } + Write-Verbose "Done receiving job named for object $jobName in state $jobState" + + } + if ($ReturnObjects) { + return $results + } + + #endregion OneObjectPerJob + + } else { + #region BatchObjectsPerJob + # Create N threads, and give X/N objects to each thread session. + + # Define script that runs per thread. + $batchScript = { + param( + [object[]]$objects, + [object]$script, + [object[]]$arguments + ) + + # Deserialize script block, turn it into a script block again. + $script = [scriptblock]::Create($script) + + # Invoke user-provided script block on each object. + # SRE-13225 - The ErrorAction on Invoke-Command does NOT affect what happens INSIDE + # the ScriptBlock. Because we're batching things to be parallelized on "shared threads" + # this ErrorAction allows us to have a failure in the middle of a batch without + # halting the entire batch. + foreach ($object in $objects) { + Invoke-Command -ErrorAction Continue -ScriptBlock $script -ArgumentList ($object, $arguments) + } + } + + # Determine how many objects to allocate to each task. Round up to get odd outliers. + $batchSize = [Math]::Ceiling($objectCount / $NumThreads); + + # Start each thread, and give each thread an allocation of objects. + for ($i = 0; $i -lt $numThreads; $i++) { + $start = $i * $batchSize + $end = (($i + 1) * $batchSize) - 1 + + $objectRange = $Objects[$start..$end] + + if ($objectRange.Count -gt 0) { + $batchName = "Batch_$($i)" + + $jobs += Start-Job -Name $batchName -ScriptBlock $batchScript -ArgumentList ($objectRange, $Script, $Arguments) -InitializationScript $InitializationScript + } + } + + # Wait for all jobs to complete. + Write-Verbose "Waiting for jobs to complete..." + Wait-Job -Job $jobs | Out-Null + + foreach ($job in $jobs) { + $jobName = $job.Name + $jobState = $job.State + Write-Verbose "Receiving job named for object $jobName in state $jobState" + if ($ReturnObjects) { + $results += Receive-Job -Job $job -ErrorAction $errorAction + + } else { + Receive-Job -Job $job -ErrorAction $errorAction + + } + + if ($CleanupJobs) { + Write-Verbose "Removing job named for object $jobName" + Remove-Job -Job $job -ErrorAction SilentlyContinue + } + Write-Verbose "Done receiving job named for object $jobName in state $jobState" + + } + + # If we want to return the output stream from jobs in a list. + if ($ReturnObjects) { + return $results + } + + #endregion BatchObjectsPerJob + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.tests.ps1 new file mode 100644 index 0000000..9d414b2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel.tests.ps1 @@ -0,0 +1,145 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe 'Invoke-Parallel' { + Context 'Ensure Correctness - Batched Parallelism' { + It 'Returns Correct Results' { + $numbers = 1..50 + + $results = Invoke-Parallel -objects $numbers -returnObjects -script { + param($number) + return $number; + }; + + # Make sure it has the right number of results. + $results.Count | Should -Be 50 + + # Make sure it returned all of the right results. + # The sum of 1-50 is 1275 + $sum = 0; + foreach($result in $results) { + $sum += $result; + } + $sum | Should -Be 1275 + } + + It 'Handles Zero Items' { + $numbers = $null; + + $results = Invoke-Parallel -objects $numbers -returnObjects -script { + param($number) + return $number; + }; + + $results | Should -Be $null; + } + + It 'Handles One Item' { + $numbers = @(1); + + $results = Invoke-Parallel -objects $numbers -returnObjects -script { + param($number) + return $number; + }; + + $results | Should -Be 1; + } + + It 'Handles ThreadCount -gt NumObjects' { + $numbers = 1..8; + $numThreads = 400; + $results = Invoke-Parallel -objects $numbers -returnObjects -numThreads $numThreads -script { + param($number) + return $number; + }; + + $results.Count | Should -Be 8; + } + + It 'Handles Odd Thread Division' { + $numbers = 1..8; + $numThreads = 3; + $results = Invoke-Parallel -objects $numbers -returnObjects -numThreads $numThreads -script { + param($number) + return $number; + }; + + $results.Count | Should -Be 8; + } + + It 'Handles Jobless Last Thread' { + # This is an edge case where Ceil(21 items / 8 threads) is rounded up to 3 items per thread. + # 7*3 == 21, which means the last thread at 8 threads doesn't have any work to do. + # There isn't a more fair way to divy up the work between the threads. + # Arbitrarily giving the 8th thread work from the 7th thread won't help in theory. + $numbers = @(1..21) + $numThreads = 8; + $results = Invoke-Parallel -objects $numbers -returnObjects -numThreads $numThreads -script { + param($number) + return $number; + } + + $results.Count | Should -Be 21; + } + } + + Context 'Ensure Correctness - Thread Per Object Parallelism' { + It 'Returns Correct Results' { + $numbers = 1..6 + + $results = Invoke-Parallel -objects $numbers -returnObjects -threadPerObject -script { + param($number) + return $number; + }; + + # Make sure it has the right number of results. + $results.Count | Should -Be 6 + + # Make sure it returned all of the right results. + # The sum of 1-6 is 21 + $sum = 0; + foreach($result in $results) { + $sum += $result; + } + $sum | Should -Be 21 + } + + It 'Handles Zero Items' { + $numbers = $null; + + $results = Invoke-Parallel -objects $numbers -returnObjects -threadPerObject -script { + param($number) + return $number; + }; + + $results | Should -Be $null; + } + + It 'Handles One Item' { + $numbers = @(1); + + $results = Invoke-Parallel -objects $numbers -returnObjects -threadPerObject -script { + param($number) + return $number; + }; + + $results | Should -Be 1; + } + + It 'Handles ThreadCount -gt NumObjects' { + $numbers = 1..8; + $numThreads = 400; + $results = Invoke-Parallel -objects $numbers -returnObjects -numThreads $numThreads -threadPerObject -script { + param($number) + return $number; + }; + + $results.Count | Should -Be 8; + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.ps1 new file mode 100644 index 0000000..07c0dbc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.ps1 @@ -0,0 +1,342 @@ +function Invoke-Parallel2 { + <# + .SYNOPSIS + Executes a script block against a list of objects in parallel with PSJobs. + Returns explicit results with Result/Object/Success/Error properties so the caller can deal with failures as they see fit. + .PARAMETER Objects + The objects to be operated on in parallel. + .PARAMETER Script + The script to be executed in parallel. The first param is implicitly one of the $objects, and the second param is your $Arguments array. + .PARAMETER Arguments + Global arguments to be handed to each job. + .PARAMETER NumThreads + The level of parallelism. + .PARAMETER ThreadPerObject + Creates a PSJob for each individual object, throttled to numThreads threads. Without it objects are divied evenly between a fixed numThread threads. + .PARAMETER StopProcessingJobsOnError + If a job fails, it will not create any more jobs. It will still return any outstanding jobs, but it will not create any more. + .PARAMETER CleanupJobs + Whether to explicitly call Remove-Job when calling Receive-Job as an attempt to manually manage memory + .NOTES + The arguments are passed to each job globally. If you want to pass different arguments to different jobs, format it into the object[] objects argument. + #> + param( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object[]]$Objects, + [Parameter(Mandatory = $true)] + [object]$Script, + [Parameter(Mandatory = $false)] + [object[]]$Arguments = $null, + [Parameter(Mandatory = $false)] + [int]$NumThreads = 8, + [Parameter(Mandatory = $false)] + [switch]$ThreadPerObject, + [Parameter(Mandatory = $false)] + [switch]$StopProcessingJobsOnError, + [Parameter(Mandatory = $false)] + [switch]$CleanupJobs + ) + process { + # Return if there are no elements to process. + if (Test-IsCollectionNullOrEmpty $Objects) { + return $null + } + + # These are all the states that are "done" as in not doing anything + # I think "Suspended" should only be for Workflows + # I think "Disconnected" should only be for PSRemoting sessions + # those will come into play for Invoke-ParallelServers + # and anywhere we use PSSessions explicitly or Invoke-Command -AsJob + # + # Why these? https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/wait-job?view=powershell-5.1#description + # {quote} + # The Wait-Job cmdlet waits for a job to be in a terminating state before continuing execution. The terminating states are: + # + # Completed + # Failed + # Stopped + # Suspended + # Disconnected + # You can wait until a specified job, or all jobs are in a terminating state. + # You can also set a maximum wait time for the job using the Timeout parameter, or use the Force parameter to wait for a job in the Suspended or Disconnected states. + # {quote} + # + # really, I'd rather put Suspended and Disconnected somewhere else and handle them differently. But I'm not sure how. + # Until I do, I'll follow the documentation. + # Not that we can do anything with Suspended and Disconnected... + + $terminatingJobStates = @( + "Completed", + "Failed", + "Stopped", + "Suspended", + "Disconnected" + ) + + + # These are all the states that are not "done" + # some are close to done, but I am not sure they can be counted + # how long does it take to get from Stopping to Stopped? + # I don't know either + $runningJobStates = @( + "Running", + "NotStarted", + "Stopping", + "Suspending" + ) + + # Wait! Where did these things come from? They weren't in the other link! + # Good catch. https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.jobstate?view=powershellsdk-1.1.0#fields + # + # I really don't know what to do with these, but they exist, and can show up. + # We shall start with "log and hope" + $wonkyJobStates = @( + "AtBreakpoint", + "Blocked" + ) + + # Cap the number of threads. + $objectCount = $Objects.Count + if ($NumThreads -gt $objectCount) { + $NumThreads = $objectCount + } + + [array]$jobs = @() + [array]$results = @() + [array]$wonkyJobs = @() + [array]$completedJobs = @() + + # foreach input object + if ($ThreadPerObject.IsPresent) { + #region OneObjectPerJob + + # Create a PSJob for each object. + $createMoreJobs = $true + foreach ($object in $Objects) { + # Wait for any job to complete if there are any. + # Also, jobs that end up "Blocked" will throw, here... + # If we have hit the max number of concurrent jobs, wait. + if ( -NOT (Test-IsCollectionNullOrEmpty -Collection $jobs) -and ($jobs.Count -ge $NumThreads)) { + + # Get the first job from the list of jobs, so we can have active-output. + # Then remove it from the list of jobs since we are going to be waiting on it. + # This has the potential to slow-down starting jobs if the workloads are "lumpy". If different jobs run in roughly the same amount of time, this isn't an issue. + # + # NOTE: Why are we doing the first by index instead of the first to finish? + # This is an odd thing to do + # Especially if this one ends up in a wonky or failed state + # Also, this never worked like Brent wanted it to - TomRowton - 2022-10-14 + $firstJob = $jobs | Select-Object -First 1 + $jobs = $jobs | Select-Object -Skip 1 + + $jobName = $firstJob.Name + $jobState = $firstJob.State + Write-Verbose "Receiving job named $jobName in state $jobState" + + $jobResult = Receive-Job -Job $firstJob -Wait + + # If we are not continuing on failure, don't create any more jobs. + if ($StopProcessingJobsOnError -and (!$jobResult.Success)) { + $createMoreJobs = $false + } + $results += $jobResult + if ($CleanupJobs) { + Write-Verbose "Removing job named $jobName" + Remove-Job -Job $firstJob -ErrorAction SilentlyContinue + } + } + + # CHECK JOB STATES + # Scrub the jobs array of jobs that have finished, and receive their outputs. + # $runningJobs = $jobs | Where-Object { $_.State -in $runningJobStates }; + # $completedJobs = $jobs | Where-Object { $_.State -in $terminatingJobStates } + $completedJobs = $jobs.Where({ + $_.State -in $terminatingJobStates + }) + $completedJobIds = $completedJobs.Id + $wonkyJobs = $jobs.Where({ + $_.State -in $wonkyJobStates + }) + $wonkyJobIds = $wonkyJobs.Id + + if ( -NOT (Test-IsCollectionNullOrEmpty -Collection $completedJobs)) { + foreach ($completedJob in $completedJobs) { + $jobName = $completedJob.Name + $jobState = $completedJob.State + Write-Verbose "Receiving job named $jobName in state $jobState" + $jobResult = Receive-Job -Job $completedJob -Wait + # If we are not continuing on failure, don't create any more jobs. + if ($StopProcessingJobsOnError -and (!$jobResult.Success)) { + $createMoreJobs = $false + } + $results += $jobResult + if ($CleanupJobs) { + Write-Verbose "Removing job named $jobName" + Remove-Job -Job $completedJob -ErrorAction SilentlyContinue + } + Write-Verbose "Done receiving job named $jobName in state $jobState" + } + } + + if (-NOT (Test-IsCollectionNullOrEmpty -Collection $wonkyJobs)) { + # Stop and Remove WONKY jobs + # where "wonky" is in the list above + foreach ($wonkyJob in $wonkyJobs) { + $jobName = $wonkyJob.Name + $jobState = $wonkyJob.State + Write-Warning "$loglead : Job named for object $jobName was in state $jobState - this is not recoverable" + Write-Warning "$loglead : Job data will be printed, job will be stopped, then removed. ErrorAction Continue is being forced." + Write-Warning "$loglead : We're all fine down here. How are you? ... Luke! We're gonna have company!" + Format-List -InputObject $wonkyJob -Property * -Force + Stop-Job -Job $wonkyJob -ErrorAction Continue + Remove-Job -Job $wonkyJob -ErrorAction Continue + Write-Warning "$loblead : Done Stopping and Removing job named for object $jobName" + + } + } + + # Repopulate the jobs array without Completed and Wonky jobs that have been Received and Removed, respectively + [array]$jobs = $jobs.Where({ + $_.Id -notin $completedJobIds -and + $_.Id -notin $wonkyJobIds + }) + + # [array]$jobs = $runningJobs + + $invokeArguments = @($object, $Script, $Arguments) + $executeScript = { + param($sb_object, $sb_script, $sb_arguments) + + # Deserialize script block, turn it into a script block again. + $sb_script = [scriptblock]::Create($sb_script) + + # Create an object to record any output from the user script, and whether it completed execution. + $sb_result = New-Object PSObject -Property @{ + Object = $sb_object + Result = $null + Success = $true + Error = $null + } + try { + $sb_result.Result = Invoke-Command -ScriptBlock $sb_script -NoNewScope -ArgumentList $sb_object, $sb_arguments + } catch { + $sb_result.Success = $false + $sb_result.Error = $_ + } + return $sb_result + } + + # Break out of the loop if something has failed, and we are not continuing on failure. + if ($StopProcessingJobsOnError -and (!$createMoreJobs)) { + break + } + + # Start a new job. + if (Test-StringIsNullOrWhitespace -Value $object.Name) { + if ($object.GetType().Name -eq "String") { + $objectName = $object + } else { + $objectName = $null + } + } else { + $objectName = $object.Name + } + Write-Verbose "Starting job for object $objectName" + $jobs += Start-Job -ScriptBlock $executeScript -ArgumentList $invokeArguments -Name $objectName + } + + # Wait for all outstanding jobs to complete and collect results. + if ( -NOT (Test-IsCollectionNullOrEmpty -Collection $jobs)) { + foreach ($job in $jobs) { + $results += Receive-Job -Job $job -Wait + } + } + + #endregion OneObjectPerJob + } else { + #region BatchObjectsPerJob + + # Write an error if anyone is telling batched-parallelism to not-continue-on-failure. + # We don't have a way of tracking failures and stopping them, because the separate threads cannot coordinate with eachother. + if ($StopProcessingJobsOnError) { + Write-Error "$logLead : -StopProcessingJobsOnError is invalid for batched parallelism." + return + } + + # Create N threads, and give X/N objects to each thread session. + + # Define script that runs per thread. + $batchScript = { + param( + [object[]]$sb_objects, + [object]$sb_script, + [object[]]$sb_arguments + ) + + # Deserialize script block, turn it into a script block again. + $sb_script = [scriptblock]::Create($sb_script) + + # Invoke user-provided script block on each object. + foreach ($object in $sb_objects) { + # Create an object to record any output from the user script, and whether it completed execution. + $sb_result = New-Object PSObject -Property @{ + Object = $object + Result = $null + Success = $true + Error = $null + } + try { + $sb_result.Result = Invoke-Command -NoNewScope -ScriptBlock $sb_script -ArgumentList ($object, $sb_arguments) + } catch { + $sb_result.Success = $false + $sb_result.Error = $_ + } + Write-Output $sb_result + } + } + + # Determine how many objects to allocate to each task. Round up to get odd outliers. + $batchSize = [Math]::Ceiling($objectCount / $NumThreads) + + # Start each thread, and give each thread an allocation of objects. + for ($i = 0; $i -lt $NumThreads; $i++) { + $start = $i * $batchSize + $end = (($i + 1) * $batchSize) - 1 + + $objectRange = $Objects[$start..$end] + + if ($objectRange.Count -gt 0) { + $jobs += Start-Job -ScriptBlock $batchScript -ArgumentList ($objectRange, $Script, $Arguments) + } + } + + # Receive-Job to output the logs. + foreach ($job in $jobs) { + [array]$resultArray = Receive-Job -Job $job -Wait + $results += $resultArray + } + + #endregion BatchObjectsPerJob + } + + #region ResultsAndOutput + # Write out errors if there are any. + # Write-Warning instead of throw so the caller can decide if they want this to stop script execution through the results. + [array]$errorResults = $results | Where-Object { !$_.Success } + if (!(Test-IsCollectionNullOrEmpty $errorResults)) { + $errorString = "$($errorResults.Count) object(s) had errors.`n" + $divider = "========================================================`n" + foreach ($result in $errorResults) { + $errorString += "$($divider)Object: $($result.Object)`nError: $($result.Error)`n" + } + $errorString += $divider + Write-Warning $errorString + } + + # Return the results that finished. + return $results + + #endregion ResultsAndOutput + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.tests.ps1 new file mode 100644 index 0000000..9f69bff --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-Parallel2.tests.ps1 @@ -0,0 +1,254 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe 'Invoke-Parallel2' { + + Context 'Ensure Correctness - Batched Parallelism' { + It 'Returns Correct Results' { + $numbers = 1..50 + + $results = Invoke-Parallel2 -objects $numbers -script { + param($number) + return $number + } + + # Make sure it has the right number of results. + $results.Count | Should -Be 50 + + # Make sure it returned all of the right results. + # The sum of 1-50 is 1275 + $sum = 0 + foreach($result in $results.Result) { + $sum += $result + } + $sum | Should -Be 1275 + } + + It 'Handles Zero Items' { + $numbers = $null + + $results = Invoke-Parallel2 -objects $numbers -script { + param($number) + return $number + } + + $results | Should -Be $null + } + + It 'Handles One Item' { + $numbers = @(1) + + $results = Invoke-Parallel2 -objects $numbers -script { + param($number) + return $number + } + + $results.Result | Should -Be 1 + } + + It 'Handles ThreadCount -gt NumObjects' { + $numbers = 1..8 + $numThreads = 400 + $results = Invoke-Parallel2 -objects $numbers -numThreads $numThreads -script { + param($number) + return $number + } + + $results.Count | Should -Be 8 + } + + It 'Handles Odd Thread Division' { + $numbers = 1..8 + $numThreads = 3 + $results = Invoke-Parallel2 -objects $numbers -numThreads $numThreads -script { + param($number) + return $number + } + + $results.Count | Should -Be 8 + } + + It 'Handles Jobless Last Thread' { + # This is an edge case where Ceil(21 items / 8 threads) is rounded up to 3 items per thread. + # 7*3 == 21, which means the last thread at 8 threads doesn't have any work to do. + # There isn't a more fair way to divy up the work between the threads. + # Arbitrarily giving the 8th thread work from the 7th thread won't help in theory. + $numbers = @(1..21) + $numThreads = 8 + $results = Invoke-Parallel2 -objects $numbers -numThreads $numThreads -script { + param($number) + return $number + } + + $results.Count | Should -Be 21 + } + } + + Context 'Ensure Correctness - Thread Per Object Parallelism' { + It 'Returns Correct Results' { + $numbers = 1..6 + + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -script { + param($number) + return $number + } + + # Make sure it has the right number of results. + $results.Count | Should -Be 6 + + # Make sure it returned all of the right results. + # The sum of 1-6 is 21 + $sum = 0 + foreach($result in $results.Result) { + $sum += $result + } + $sum | Should -Be 21 + } + + It 'Handles Zero Items' { + $numbers = $null + + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -script { + param($number) + return $number + } + + $results | Should -Be $null + } + + It 'Handles One Item' { + $numbers = @(1) + + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -script { + param($number) + return $number + } + + $results.Result | Should -Be 1 + } + + It 'Handles ThreadCount -gt NumObjects' { + $numbers = 1..8 + $numThreads = 400 + $results = Invoke-Parallel2 -objects $numbers -numThreads $numThreads -threadPerObject -script { + param($number) + return $number + } + + $results.Count | Should -Be 8 + } + } + + Context 'Ensure Correctness - Thread Per Object Error Handling' { + + It 'Returns Error Results' { + + $numbers = 1..4 + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -ErrorAction SilentlyContinue -script { + param($number) + throw "Failure!" + } + + # All of the results should fail, and contain the correct error. + $badResults = $results | Where-Object { $_.Success -eq $false } + $badResults.Count | Should -Be 4 + for($i = 0; $i -lt 4; $i++) { + $badResults[$i].Error | Should -Be "Failure!" + } + } + + It 'Returns All Results When ContinueOnFailure is True' { + + $numbers = 1..4 + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -numThreads 1 -ErrorAction SilentlyContinue -script { + param($number) + throw "Failure!" + } + + # All of the results should fail, and contain the correct error. + $badResults = $results | Where-Object { $_.Success -eq $false } + $badResults.Count | Should -Be 4 + for($i = 0; $i -lt 4; $i++) { + $badResults[$i].Error | Should -Be "Failure!" + } + } + + + It 'Returns One Result With StopProcessingJobsOnError' { + $numbers = 1..4 + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -numThreads 1 -ErrorAction SilentlyContinue -StopProcessingJobsOnError -script { + param($number) + throw "Failure!" + } + + # Only one result should be returned because we are at a parallelism of 1, and we are -not- continuing on error. + [array]$badResults = $results | Where-Object { $_.Success -eq $false } + $badResults.Count | Should -Be 1 + $badResults.Error | Should -Be "Failure!" + } + + It 'Returns Outstanding Thread Results and Doesnt Create More' { + $numbers = 1..6 + $results = Invoke-Parallel2 -objects $numbers -threadPerObject -numThreads 3 -ErrorAction SilentlyContinue -StopProcessingJobsOnError -script { + param($number) + + if($number -eq 1) { + throw "Failure!" + } else { + # Make sure the other non-failing results take a little time, so that more jobs are not spawned. + Start-Sleep -Seconds 2 + } + } + + [array]$goodResults = $results | Where-Object { $_.Success -eq $true } + [array]$badResults = $results | Where-Object { $_.Success -eq $false } + + # We should have 3 results. + # .. because we are at a parallelism of 3 jobs (out of 6 objects) + # .. and one of them fails. Two should succeed, one should fail, and we should not create any more jobs. + $results.Count | Should -Be 3 + $goodResults.Count | Should -Be 2 + $badResults.Count | Should -Be 1 + } + } + + Context 'Ensure Correctness - Batched Error Handling' { + + It 'Returns Success Results' { + + $numbers = 1..4 + $results = Invoke-Parallel2 -objects $numbers -ErrorAction SilentlyContinue -script { + param($number) + # Do nothing. All good. + } + + # All of the results should fail, and contain the correct error. + $goodResults = $results | Where-Object { $_.Success -eq $true } + $goodResults.Count | Should -Be 4 + for($i = 0; $i -lt 4; $i++) { + $goodResults[$i].Error | Should -Be $null + } + } + + It 'Returns All Results When There is Failure' { + + $numbers = 1..4 + $results = Invoke-Parallel2 -objects $numbers -numThreads 1 -ErrorAction SilentlyContinue -script { + param($number) + throw "Failure!" + } + + # All of the results should fail, and contain the correct error. + $badResults = $results | Where-Object { $_.Success -eq $false } + $badResults.Count | Should -Be 4 + for($i = 0; $i -lt 4; $i++) { + $badResults[$i].Error | Should -Be "Failure!" + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-ParallelServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-ParallelServers.ps1 new file mode 100644 index 0000000..781aa98 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-ParallelServers.ps1 @@ -0,0 +1,97 @@ +function Invoke-ParallelServers { +<# +.SYNOPSIS + Executes a script block against a list of servers in parallel with PSJobs + remote sessions. + Can return results if the ReturnObjects switch is supplied + +.PARAMETER Servers + [string[]] List of fully qualified server addresses to apply this across + +.PARAMETER Script + [ScriptBlock] The scriptblock to execute + +.PARAMETER Arguments + [object[]] The arguments to be passed into the inner scriptblock + +.PARAMETER NumThreads + [int] Defualts to 30 + +.PARAMETER ReturnObjects + [switch] Do we return results after the run? Legacy functionality is to swallow the Write-Output and return $XYZ values. Other "return" values like Write-Host streams continue to be returned in either option. + +.NOTES + The arguments are passed to each job globally. If you want to pass different arguments to different jobs, format it into the object[] list argument. +#> + param( + [Parameter(Mandatory=$true)] + [string[]]$Servers, + + [Parameter(Mandatory=$true)] + [object]$Script, + + [Parameter(Mandatory=$false)] + [object[]]$Arguments = $null, + + [Parameter(Mandatory=$false)] + [int]$NumThreads = 30, + + [Parameter(Mandatory=$false)] + [switch]$ReturnObjects + ) + process { + $jobs = @() + + # Define script block to create remote session, and execute the script block parameter on the remote host. + $ScriptBlock = { + param($server, $innerScript, $passedArguments) + process { + + $remoteBlock = [scriptblock]::Create($innerScript) + + $session = New-PSSession $server -ErrorAction SilentlyContinue + if($session.ComputerName -ne $server) + { + throw "Could not connect to $server" + } + + return Invoke-Command -Session $session -ScriptBlock $remoteBlock -ArgumentList $passedArguments + } + end + { + Remove-PSSession $session + } + } + + $logLead = (Get-LogLeadName); + + # For each server. + foreach($server in $Servers) + { + Write-Verbose ("$logLead : Starting job for host {0}" -f $server) + $jobs += Start-Job -ScriptBlock $ScriptBlock -ArgumentList $server,$Script,$Arguments + + $running = @($jobs | Where-Object {$_.State -in ('Running','NotStarted')}) + + while ($running.Count -ge $NumThreads -and $running.Count -ne 0) + { + (Wait-Job -Job $jobs -Any) | Out-Null + $running = @($jobs | Where-Object {$_.State -in ('Running','NotStarted')}) + } + } + + (Wait-Job -Job $jobs) | Out-Null + + $results = @() + # Receive-Job to output the logs. + # SRE-13225 - Receive-Job dumps all of the output streams from a job to the current PS "host" + # When one of those is an "error" it stops because our $ErrorActionPreference is "Stop" + # To get all of the output from these parallelized jobs, we need to "Continue" + # All the jobs have already finished executing at this point. This does not alter that. + # This merely allows us to see all of the output (unless there's a throw?) + $jobs | ForEach-Object { $results += (Receive-Job -Job $_ -ErrorAction Continue) } + + if($ReturnObjects) { + return $results + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Invoke-QueryOnClientDatabase.ps1 b/Modules/Alkami.PowerShell.Common/Public/Invoke-QueryOnClientDatabase.ps1 new file mode 100644 index 0000000..952d2ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Invoke-QueryOnClientDatabase.ps1 @@ -0,0 +1,40 @@ +function Invoke-QueryOnClientDatabase { +<# +.SYNOPSIS + Runs a Query Against a Client Database Object +#> + param ( + [Parameter(Position = 0, Mandatory = $true)] + [PSObject]$Client, + + [Parameter(Position = 1, Mandatory = $true)] + [string]$QueryString + ) + + $conn = New-Object System.Data.SqlClient.SqlConnection + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($Client.ConnectionString) + $conn.ConnectionString = $conStrBuilder.ToString() + Write-Verbose ("Connecting to master database with connection string {0}" -f $conStrBuilder.ToString()) + + try { + $conn.Open() + $query = New-Object System.Data.SqlClient.SqlCommand($QueryString, $conn) + $result = $query.ExecuteScalar() + + $result + } + catch { + Write-Warning "An exception occurred while trying to execute the specified query against the client database" + Write-Warning $error[0] | Format-List -Force + return $null + } + finally { + if ($conn.State -ne [System.Data.ConnectionState]::Closed) { + $conn.Close() + } + + $conn = $null + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.ps1 b/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.ps1 new file mode 100644 index 0000000..8c51a97 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.ps1 @@ -0,0 +1,94 @@ +function Move-LogsAndDeleteDotNetTemps { +<# +.SYNOPSIS + Deletes or archives logs based off a cutoff date and then cleans the .NET temporary files + +.DESCRIPTION + By default, deletes current and rolled logs in a given folder, then, if not on a FAB host, + cleans .NET temp files and archives chocolatey logs and prunes old chocolatey log archives. + +.PARAMETER logDirectory + String represenation of the path to the folder that should be cleaned up. + Optional. If omitted, Get-OrbLogsPath is used instead + +.PARAMETER cutoffDays + Number of days of past log archives to retain. Anything older will be deleted + +.PARAMETER forceRecycle + Switch to clean up non-FAB host logs and temps even if there are Alkami worker processes + running + +.PARAMETER skipActiveLogs + Switch to ignore any currently active logs (ones that have not rolled) in the case of + either a FAB server, where services never stop, or an element deploy or "hot" tier restart + +.PARAMETER ArchiveLogFiles + Switch to enable zipping up of log files, instead of simple deleting + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("LogPath")] + [string]$logDirectory, + + [Parameter(Mandatory=$false)] + [Alias("CutoffThreshold")] + [int]$cutoffDays = 1, + + [Parameter(Mandatory=$false)] + [Alias("Force")] + [switch]$forceRecycle, + + [Parameter(Mandatory=$false)] + [Alias("SkipActive")] + [switch]$skipActiveLogs, + + [Parameter(Mandatory=$false)] + [switch]$ArchiveLogFiles + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrEmpty($logDirectory)) { + $logDirectory = (Get-OrbLogsPath) + } + + if (Test-IsServiceFabricServer) { + Write-Warning "$logLead : FAB tier servers do not have .net Temps" + if ($ArchiveLogFiles) { + Write-Warning "$logLead : Calling Backup-ORBLogFiles -SkipActiveLogs" + Backup-AlkamiLogs -skipActiveLogs -CutoffThreshold $cutoffDays -LogPath $logDirectory + } else { + # Delete current log files but -skipActiveLogs (regardless of passed param) + Write-Warning "$logLead : Skipped Backup, deleting current logs ONLY(FAB)" + Remove-ORBLogFiles -SkipActiveLogs -LogPath $logDirectory + + } + return + } + + if ((Search-ForRunningWorkerProcesses) -or $forceRecycle) { + + Remove-DotNetTemporaryFiles + + if ($ArchiveLogFiles) { + Backup-AlkamiLogs -skipActiveLogs:$skipActiveLogs -CutoffThreshold $cutoffDays -LogPath $logDirectory + # Backup-ORBLogFiles -skipActiveLogs:$skipActiveLogs + # Remove-OldArchivedLogFiles -ArchivePath (Join-Path $logDirectory "Archive") -CutoffThreshold $cutoffDays + } else { + # Delete current log files but -skipActiveLogs (regardless of passed param) + Write-Warning "$logLead : Skipped Backup, deleting current logs ONLY" + Remove-ORBLogFiles -SkipActiveLogs:$skipActiveLogs -LogPath $logDirectory + + } + + $chocoInstallPath = Get-ChocolateyInstallPath + $chocoDirectory = Join-Path $chocoInstallPath "logs" + Backup-LogFiles -logDirectory $chocoDirectory -CutoffDays 7 # Preserve choco logs for a week. + Remove-OldArchivedLogFiles -ArchivePath (Join-Path $chocoDirectory "Archive") -CutoffThreshold 14 # Preserve 2 weeks of logs. + } else { + + Write-Warning ("$logLead : Alkami worker processes are still running. Cannot continue.") + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.tests.ps1 new file mode 100644 index 0000000..842f5f6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Move-LogsAndDeleteDotNetTemps.tests.ps1 @@ -0,0 +1,223 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Move-LogsAndDeleteDotNetTemps" { + + # CODEPATH: ALWAYS + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Default]"} + + # CODEPATH: No LogDirectory param + Mock -ModuleName $moduleForMock -CommandName Get-OrbLogsPath -MockWith {return "C:\temp\OrbLogs"} + + # CODEPATH: FAB + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + + # CODEPATH: Not FAB AND (No running Alkami workers OR forceRecycle) + # CONDITION: Backwards logic. $true means there are NOT any alkami worker processes running + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $true} + + # ACTION: Remove dotnet temps + Mock -ModuleName $moduleForMock -CommandName Remove-DotNetTemporaryFiles -MockWith {} + + # ACTION: Either backup logs or remove logs + # Backup + Mock -ModuleName $moduleForMock -CommandName Backup-AlkamiLogs -MockWith {} + # Remove + Mock -ModuleName $moduleForMock -CommandName Remove-ORBLogFiles -MockWith {} + + # ACTION: Choco cleanup + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyInstallPath -MockWith {return "C:\temp\choco"} + Mock -ModuleName $moduleForMock -CommandName Backup-LogFiles -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Remove-OldArchivedLogFiles -MockWith {} + + + # CODEPATH: Not FAB AND (EITHER running Alkami workers OR forceRecycle) + # ACTION: Just warn + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + # NOTE: Mocks for Get-LogLeadName are left in below to enable easy test debugging in the future. + # You can just comment out this Mock for Write-Warning and start getting (some) warnings + # with test-specific $logLeads allowing you to see which codepath you are (supposed to be) on. + + Context "Parameters_And_Codepaths" { + It "Get_LogLead" { + + Move-LogsAndDeleteDotNetTemps + + Assert-MockCalled -CommandName Get-LogLeadName -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + } + It "Get_GetOrbLogsPath_If_LogDirectory_Omitted" { + + Move-LogsAndDeleteDotNetTemps + + Assert-MockCalled -CommandName Get-OrbLogsPath -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + } + It "Skip_GetOrbLogsPath_If_LogDirectory_Passed" { + + Move-LogsAndDeleteDotNetTemps -LogDirectory "C:\Temp\NotOrbLogs" + + Assert-MockCalled -CommandName Get-OrbLogsPath -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + } + It "FabHost_Defaults_DeleteLogsSkipActive" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Fab_Default]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Fab_Default]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$SkipActiveLogs -eq $true} + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + } + It "FabHost_ArchiveLogFilesFlag_ArchiveLogsSkipActive" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Fab_ArchiveLogFiles]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Fab_ArchiveLogFiles]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps -ArchiveLogFiles + + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$SkipActiveLogs -eq $true} + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + } + It "NonFabHost_NoAlkamiServicesRunning_NoForceRecycle_Defaults_DeleteLogs_DeleteDotNetTemps_ArchiveAndCleanChocoLogs" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_Default]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_Default]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps -ForceRecycle:$false + + Assert-MockCalled -CommandName Remove-DotNetTemporaryFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$SkipActiveLogs -eq $false} + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + + Assert-MockCalled -CommandName Backup-LogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$logDirectory -eq "c:\temp\choco\logs"} + Assert-MockCalled -CommandName Remove-OldArchivedLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$archiveDirectory -eq "c:\temp\choco\logs\Archive"} + + } + It "NonFabHost_ArchiveLogFilesFlag_NoAlkamiServicesRunning_NoForceRecycle_ArchiveLogs_DeleteDotNetTemps_ArchiveAndCleanChocoLogs" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_Default]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_Default]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps -ForceRecycle:$false -ArchiveLogFiles + + Assert-MockCalled -CommandName Remove-DotNetTemporaryFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$SkipActiveLogs -eq $false} + + Assert-MockCalled -CommandName Backup-LogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$logDirectory -eq "c:\temp\choco\logs"} + Assert-MockCalled -CommandName Remove-OldArchivedLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$archiveDirectory -eq "c:\temp\choco\logs\Archive"} + + } + It "NonFabHost_AlkamiServicesRunning_NoForceRecycle_WarnOnly" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} ` + -ParameterFilter {$Message -match "Cannot continue"} + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_ServicesRunning]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_ServicesRunning]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $false} + + Move-LogsAndDeleteDotNetTemps + + Assert-MockCalled -CommandName Remove-DotNetTemporaryFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly -ParameterFilter {$SkipActiveLogs -eq $false} + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + + Assert-MockCalled -CommandName Backup-LogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly -ParameterFilter {$logDirectory -eq "c:\temp\choco\logs"} + Assert-MockCalled -CommandName Remove-OldArchivedLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly -ParameterFilter {$archiveDirectory -eq "c:\temp\choco\logs\Archive"} + + Assert-MockCalled -CommandName Write-Warning -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$Message -match "Alkami worker processes are still running"} + + } + It "NonFabHost_NoAlkamiServicesRunning_ForceRecycle_Defaults_DeleteLogs_DeleteDotNetTemps_ArchiveAndCleanChocoLogs" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_NoServicesRunning]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_NoServicesRunning]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps -forceRecycle + + Assert-MockCalled -CommandName Remove-DotNetTemporaryFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$SkipActiveLogs -eq $false} + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + + Assert-MockCalled -CommandName Backup-LogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$logDirectory -eq "c:\temp\choco\logs"} + Assert-MockCalled -CommandName Remove-OldArchivedLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$archiveDirectory -eq "c:\temp\choco\logs\Archive"} + + } + It "NonFabHost_ArchiveLogFilesFlag_NoAlkamiServicesRunning_ForceRecycle_ArchiveLogs_DeleteDotNetTemps_ArchiveAndCleanChocoLogs" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_NoServicesRunning]"} + Mock -ModuleName Alkami.DevOps.Operations -CommandName Get-LogLeadName -MockWith {return "[UnitTest_NonFab_NoServicesRunning]"} + + Mock -ModuleName $moduleForMock -CommandName Test-IsServiceFabricServer -MockWith {return $false} + Mock -ModuleName $moduleForMock -CommandName Search-ForRunningWorkerProcesses -MockWith {return $true} + + Move-LogsAndDeleteDotNetTemps -forceRecycle -ArchiveLogFiles + + Assert-MockCalled -CommandName Remove-DotNetTemporaryFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + + Assert-MockCalled -CommandName Remove-ORBLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly -ParameterFilter {$SkipActiveLogs -eq $false} + Assert-MockCalled -CommandName Backup-AlkamiLogs -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + + Assert-MockCalled -CommandName Backup-LogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$logDirectory -eq "c:\temp\choco\logs"} + Assert-MockCalled -CommandName Remove-OldArchivedLogFiles -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly -ParameterFilter {$archiveDirectory -eq "c:\temp\choco\logs\Archive"} + + } + } + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/New-7Zip.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-7Zip.ps1 new file mode 100644 index 0000000..e72ebbc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-7Zip.ps1 @@ -0,0 +1,15 @@ +function New-7Zip { +<# +.SYNOPSIS + Creates a 7-Zip Archive from a List of Files +#> + param( + $ZipFileName, + $OutDirectory, + $FilesToAdd + ) + + [Array]$arguments = "a", "-tzip", "$ZipFileName", "-o $OutDirectory", "-bd", "-y", $FilesToAdd + & $pathToZipExe $arguments +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/New-AlkamiEventSource.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-AlkamiEventSource.ps1 new file mode 100644 index 0000000..b409dc5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-AlkamiEventSource.ps1 @@ -0,0 +1,22 @@ +function New-AlkamiEventSource { +<# +.SYNOPSIS + Creates a New Event Source Under the AlkamiOps Log +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$sourceName + ) + + $logLead = (Get-LogLeadName); + + if (!Test-IsAdmin) { + Write-Warning ("$logLead : Unable to Create Event Source {0} as the User Is Not Running as an Administrator" -f $sourceName) + return + } + + New-EventLog -LogName AlkamiOps -Source $sourceName -ErrorAction SilentlyContinue +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/New-AlkamiModule.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-AlkamiModule.ps1 new file mode 100644 index 0000000..f80b374 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-AlkamiModule.ps1 @@ -0,0 +1,290 @@ +function New-AlkamiModule { +<# + +.SYNOPSIS + This function creates a new module according to the Alkami pattern. + +.DESCRIPTION + This function creates a new module according to the Alkami pattern. + This function will create the entire folder structure as well as a default pair of functions for testing. + +.PARAMETER ModuleName + [string] The name of the module to be created + +.INPUTS + Requires the ModuleName to be provided. Expects a 3 part dotted name, will use the last two parts when split by period + +.OUTPUTS + This function creates a folder and files under the current location. + +.EXAMPLE + New-AlkamiModule -ModuleName MyModule + +New-AlkamiModule -ModuleName MyModule + +.EXAMPLE + New-AlkamiModule -ModuleName "Alkami.PowerShell.MyModule" + +New-AlkamiModule -ModuleName "Alkami.PowerShell.MyModule" + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ModuleName + ) + process { + if ([string]::IsNullOrWhiteSpace($ModuleName)) { + throw "ModuleName must be supplied" + } + + ## If we don't specify a module name, assume we are creating a default Alkami.PowerShell module + if ($ModuleName.IndexOf('.') -eq -1) { + $ModuleName = "Alkami.PowerShell.$ModuleName" + } + + $workingRoot = (Get-Location) + + $newModuleRootFolder = (Join-Path $workingRoot $ModuleName) + $psd1Path = (Join-Path $newModuleRootFolder "$ModuleName.psd1") + $projectPath = (Join-Path $newModuleRootFolder "$ModuleName.pssproj") + $nuspecPath = (Join-Path $newModuleRootFolder "$ModuleName.nuspec") + $publicFolder = (Join-Path $newModuleRootFolder Public) + $privateFolder = (Join-Path $newModuleRootFolder Private) + $publicDemoFile = (Join-Path $publicFolder "Get-HelloWorld.ps1") + $privateDemoFile = (Join-Path $privateFolder "Get-HelloWorldInternal.ps1") + $toolsFolder = (Join-Path $newModuleRootFolder tools) + $chocoInstallPath = (Join-Path $toolsFolder "chocolateyInstall.ps1") + $chocoUninstallPath = (Join-Path $toolsFolder "chocolateyUninstall.ps1") + + if (Test-Path $psd1Path) { + throw "$ModuleName already exists, aborting" + } + + $getHelloWorldContent = @" +function Get-HelloWorld { +<# + +.SYNOPSIS + This function is just to demonstrate a Hello World in the new module being created + +.DESCRIPTION + This function is just to demonstrate a Hello World in the new module being created + +.INPUTS + None + +.OUTPUTS + Hello World + +.EXAMPLE + Get-HelloWorld + +Get-HelloWorld +Hello World +#> + [CmdletBinding()] + param( + ) + process { + Get-HelloWorldInternal + } +} +"@ + + $getHelloWorldPrivateContent = @" +function Get-HelloWorldInternal { + return "Hello World" +} +"@ + + $randomGuid = [guid]::NewGuid() + $author = $env:USERNAME + $copyrightYear = [DateTime]::Now.Year + $moduleContent = @" +@{ + RootModule = '$ModuleName.psm1' + ModuleVersion = '1.0.0' + GUID = '$randomGuid' + Author = '$author' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) $copyrightYear Alkami Technologies, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = '' +} +"@ + $middlePart = ($ModuleName -split '\.')[-2] + $endPart = ($ModuleName -split '\.')[-1] + + $nuspecContent = @" + + + + $ModuleName + `$version`$ + Alkami Platform Modules - $middlePart - $endPart + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/$ModuleName + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami $endPart module for use with PowerShell. + + PowerShell + Copyright (c) $copyrightYear Alkami Technologies + + + + + + + + + + +"@ + + $chocoInstallContent = @" +[CmdletBinding()] +Param() +process { + `$myCurrentPath = `$PSScriptRoot; + Write-Verbose "Installing the Module from `$myCurrentPath"; + + `$parentPath = (Split-Path `$myCurrentPath); + `$systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + `$myModulePath = (Join-Path `$parentPath "module"); + + `$metadata = ([Xml](Get-Content (Join-Path `$parentPath "*.nuspec"))).package.metadata; + + `$id = `$metadata.id; + `$version = `$metadata.version -replace '-pre.+',''; + + `$targetModulePath = (Join-Path `$systemModulePath `$id); + `$targetModuleVersionPath = (Join-Path `$targetModulePath `$version); + + if (Test-Path `$targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path `$targetModuleVersionPath) { + Write-Warning "Found an already existing module at [`$targetModuleVersionPath]!!" + Remove-Item `$targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem `$targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [`$_]"; + Remove-Item `$_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module `$id to [`$targetModuleVersionPath]"; + Copy-Item `$myModulePath -Destination `$targetModuleVersionPath -Recurse -Force; + + ## Ensure the module was able to load + Import-Module `$id -Global; +} +"@ + + $chocoUninstallContent = @" +[CmdletBinding()] +Param() +process { + `$myCurrentPath = `$PSScriptRoot; + Write-Verbose "Uninstalling the Module from `$myCurrentPath"; + + `$parentPath = (Split-Path `$myCurrentPath); + `$systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + `$myModulePath = (Join-Path `$parentPath "module"); + + `$metadata = ([Xml](Get-Content (Join-Path `$parentPath "*.nuspec"))).package.metadata; + + `$id = `$metadata.id; + `$version = `$metadata.version -replace '-pre.+',''; + + `$targetModulePath = (Join-Path `$systemModulePath `$id); + `$targetModuleVersionPath = (Join-Path `$targetModulePath `$version); + + if (Test-Path `$targetModuleVersionPath) { + Write-Information "Removing module at [`$targetModuleVersionPath]!!" + Remove-Item `$targetModuleVersionPath -Recurse -Force; + } +} +"@ + +$projectGuid = [Guid]::NewGuid() +$projectContent = @" + + + Debug + 2.0 + {$projectGuid} + Exe + $ModuleName + $ModuleName + $ModuleName + Invoke-Pester; + ..\build-project.ps1 (Join-Path `$(SolutionDir) "$ModuleName") + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + +"@ + + if (!(Test-Path $newModuleRootFolder)) { + New-Item -Path $newModuleRootFolder -ItemType Directory -Force + } + + if (!(Test-Path $publicFolder)) { + New-Item -Path $publicFolder -ItemType Directory -Force + } + + if (!(Test-Path $privateFolder)) { + New-Item -Path $privateFolder -ItemType Directory -Force + } + + if (!(Test-Path $toolsFolder)) { + New-Item -Path $toolsFolder -ItemType Directory -Force + } + + Set-Content -Path $publicDemoFile -Value $getHelloWorldContent + + Set-Content -Path $privateDemoFile -Value $getHelloWorldPrivateContent + + Set-Content -Path $psd1Path -Value $moduleContent + + Set-Content -Path $nuspecPath -Value $nuspecContent + + Set-Content -Path $chocoInstallPath -Value $chocoInstallContent + + Set-Content -Path $chocoUninstallPath -Value $chocoUninstallContent + + Set-Content -Path $projectPath -Value $projectContent + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/New-DynamoMessageStringValue.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-DynamoMessageStringValue.ps1 new file mode 100644 index 0000000..f28d61b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-DynamoMessageStringValue.ps1 @@ -0,0 +1,21 @@ +function New-DynamoMessageStringValue { + <# + .SYNOPSIS + Create an Dynamo AttributeValue with a string value. Resulting AttribueValue object can be modified if other data types + are required. + + .PARAMETER Value + Attribute Value. + #> + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [object]$Value + ) + Import-AWSModule + + $attribute = New-Object -TypeName Amazon.DynamoDBv2.Model.AttributeValue($Value) + # This typecast is required, because Powershell. Do not remove it. + return [Amazon.DynamoDBv2.Model.AttributeValue]$attribute + } + + Set-Alias -Name AsDynamoMessageValue -Value New-DynamoMessageStringValue \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/New-SNSMessageAttribute.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-SNSMessageAttribute.ps1 new file mode 100644 index 0000000..be5a467 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-SNSMessageAttribute.ps1 @@ -0,0 +1,34 @@ +function New-SNSMessageAttribute { +<# +.SYNOPSIS + Create an SNSMessageAttribute + +.PARAMETER Value + Attribute Value. + +.PARAMETER DataType + Data Type of the attribute. Possible options are String, String.Array, Number, and Binary. Only String and Binary are + valid for triggering Lambda functions + +.PARAMETER AttributeValueName + Name of the attribute data field. Valid options are StringValue or BinaryValue +#> + + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [object]$Value, + [ValidateSet("String", "String.Array", "Number", "Binary")] + [string]$DataType = "String", + [ValidateSet("StringValue", "BinaryValue")] + [string]$AttributeValueName = "StringValue" + ) + Import-AWSModule + + $attribute = New-Object -TypeName Amazon.SimpleNotificationService.Model.MessageAttributeValue + $attribute.DataType = $DataType + $attribute.$AttributeValueName = $Value + # This typecast is required, because Powershell. Do not remove it. + return [Amazon.SimpleNotificationService.Model.MessageAttributeValue]$attribute +} + +Set-Alias -Name AsSNSAttribute -Value New-SNSMessageAttribute \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/New-StatusIoIncident.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-StatusIoIncident.ps1 new file mode 100644 index 0000000..3204bf5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-StatusIoIncident.ps1 @@ -0,0 +1,58 @@ +Function New-StatusIoIncident { + <# + .SYNOPSIS + Sends an SNS message to create a new StatusPage.Io Incident. + + .PARAMETER PageId + StatusIo Page Id. Alkami's account id. + + .PARAMETER ComponentId + Specific Component to modify. Often a pod or lane. + + .PARAMETER ComponentName + Human friendly Component name + + .PARAMETER ComponentStatus + Component status to set + + .PARAMETER IncidentName + Human friendly Incident Name + + .PARAMETER IncidentStatus + Incident status to set. + + .PARAMETER IncidentId + Incident Id of an existing incident to update. Ignored if it doesn't exist. If it's invalid, the message will + fire, but the lambda will fail. Check those logs if updates aren't occurring. Ignored if provided for a Create + incident. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$PageId, + [Parameter(Mandatory = $true)] + [string]$ComponentId, + [Parameter(Mandatory = $true)] + [string]$ComponentName, + [Parameter(Mandatory = $true)] + [string]$ComponentStatus, + [Parameter(Mandatory = $true)] + [string]$IncidentName, + [Parameter(Mandatory = $true)] + [string]$IncidentStatus, + [Parameter(Mandatory = $false)] + [string]$IncidentId + ) + + $incident = @{ + PageId = $pageId; + ComponentId = $ComponentId; + ComponentName = $ComponentName; + ComponentStatus = $ComponentStatus; + IncidentName = $IncidentName; + IncidentStatus = $IncidentStatus; + InciidentId = $IncidentId; + } + + return $incident +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/New-Symlink.Tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-Symlink.Tests.ps1 new file mode 100644 index 0000000..23aa4b8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-Symlink.Tests.ps1 @@ -0,0 +1 @@ +# New-Symlink has integration tests over in Remove-FileSystemItem.Tests.ps1 \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/New-Symlink.ps1 b/Modules/Alkami.PowerShell.Common/Public/New-Symlink.ps1 new file mode 100644 index 0000000..19066e6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/New-Symlink.ps1 @@ -0,0 +1,52 @@ +Function New-Symlink { +<# +.SYNOPSIS + Create a new symlink. This function is to make my life as a SRE or developer a lot easier. + +.PARAMETER ActualFilePath + [string] The place where the file is actually at + +.PARAMETER TargetFilePath + [string] The place where the file should appear to be + +.PARAMETER TargetName + [string] The name to use for the target. If not supplied, will match the filename or folder name of the ActualFilePath (Path) + +#> + [CmdletBinding()] + Param( + [Alias("Path")] + [Alias("Source")] + [Parameter(Mandatory = $true)] + [string]$ActualFilePath, + + [Alias("Destination")] + [Alias("Target")] + [Parameter(Mandatory = $true)] + [string]$TargetFilePath, + + [Alias("Name")] + [Parameter(Mandatory = $false)] + [string]$TargetName + ) + + $loglead = (Get-LogLeadName) + + $ActualFilePath = Resolve-Path $ActualFilePath + + if (!(Test-Path $ActualFilePath)) { + Write-Warning "$logLead : The requested path to be linked doesn't actually exist on disk [$ActualFilePath]" + return + } + + $actualLeaf = (Split-Path $ActualFilePath -Leaf) + $targetLeaf = (Split-Path $TargetFilePath -Leaf) + $leafNamesMatch = $actualLeaf -eq $targetLeaf + + if ([string]::IsNullOrWhiteSpace($TargetName) -and !$leafNamesMatch) { + $TargetName = $actualLeaf + } + + Write-Host "$logLead : New-Item -ItemType SymbolicLink -Path $TargetFilePath -Value $ActualFilePath -Name $TargetName -Force" + (New-Item -ItemType SymbolicLink -Path $TargetFilePath -Value $ActualFilePath -Name $TargetName -Force) | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Open-UrlInDefaultBrowser.ps1 b/Modules/Alkami.PowerShell.Common/Public/Open-UrlInDefaultBrowser.ps1 new file mode 100644 index 0000000..d348fdd --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Open-UrlInDefaultBrowser.ps1 @@ -0,0 +1,57 @@ +function Open-UrlInDefaultBrowser { +<# +.SYNOPSIS + Opens the Given URL in the User's Default Browser +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$url + ) + + $logLead = (Get-LogLeadName); + $edgeBrowserIdHash = "AppXq0fevzme2pys62n3e0fbqa7peapykr8v" + + [System.Uri]$uri = $null + if (!([System.URI]::TryCreate($url, [System.UriKind]::Absolute, [ref]$uri) -and ($uri.Scheme -eq [System.Uri]::UriSchemeHttp -or $uri.Scheme -eq [System.Uri]::UriSchemeHttps))) { + Write-Warning ("$logLead : The provided URL -- {0} -- could not be parsed as an absolute URI. Be sure to pass in the URL prefix and a full URL" -f $url) + return + } + #Gets default browser from regedit; if there is none, sets to IE.HTTP; Chrome is ChromeHTML + #TODO set this up for https as well + $HKCUPath = "HKCU:\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice" + $browserIdRaw = Get-ItemPropertyValue -Name ProgId -Path $HKCUPath -ErrorAction SilentlyContinue + + if(!$browserIdRaw -or $browserIdRaw -eq $edgeBrowserIdHash) + { + $browserId = "IE.HTTP" + } + else + { + $browserId = Get-ItemPropertyValue -Name ProgId -Path $HKCUPath -ErrorAction SilentlyContinue + } + + try { + + Write-Verbose ("$logLead : Browser ID read as {0}" -f $browserId) + + New-PSDrive -PSProvider registry -Root 'HKEY_CLASSES_ROOT' -Name 'HKCR' | Out-Null + $browserCmd = (Get-Item "HKCR:\$browserId\shell\open\command" | Get-ItemProperty | Select-Object -ExpandProperty "(default)") + Write-Verbose ("$logLead : Raw browser command path read as {0}" -f $browserCmd) + + if ($browserCmd -match '\".+?\"') { + return (Start-Process -FilePath $matches[0] -ArgumentList $url -PassThru).Id + } + else { + Write-Warning ("$logLead : Could not read the browser command path from the registry") + } + } + catch { + Write-Warning ("$logLead : An unexpected exception occurred while opening the URL: {0}" -f $_.Exception.Message) + } + finally { + Remove-PSDrive -Name "HKCR" + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Out-FileWithRetry.ps1 b/Modules/Alkami.PowerShell.Common/Public/Out-FileWithRetry.ps1 new file mode 100644 index 0000000..deb7a76 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Out-FileWithRetry.ps1 @@ -0,0 +1,48 @@ +function Out-FileWithRetry { +<# +.SYNOPSIS + Use this instead of Tee-Object because this actually lets you bypass errors to some regard + +.PARAMETER FilePath + [string] File path to write to + +.PARAMETER InputObject + [PSObject] Pipeline fed object + +.PARAMETER Append + [switch] Used to indicate if the file should be overwritten or appended. Force is assumed in either regard. + +.EXAMPLE + "My log message" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host +#> + [CmdletBinding()] + [OutputType([PSObject])] + param( + [Parameter(Mandatory=$true, Position = 0)] + [string]$FilePath, + [Parameter(Mandatory=$true,ValueFromPipeline = $true)] + [PSObject]$InputObject, + [switch]$Append + ) + begin { + $logLead = (Get-LogLeadName) + $FilePathValid = Test-Path $FilePath -IsValid + } + process { + if (!$FilePathValid) { + Write-Warning "$logLead : Can not log to FilePath [$FilePath] as it is invalid. Still passing output on the pipeline." + } + + # Only write if the path was valid in the first place, obviously + if ($FilePathValid) { + Invoke-CommandWithRetry -Arguments @($FilePath, $InputObject, $Append) -Milliseconds 100 -JitterMin -10 -JitterMax 100 -ScriptBlock { + param($FilePath, $InputObject, $useAppend) + $InputObject | Out-File -Append:$useAppend -Force -FilePath $FilePath + } + } + + return $InputObject + } +} + +Set-Alias -Name Tee-OutFile -Value Out-FileWithRetry diff --git a/Modules/Alkami.PowerShell.Common/Public/Read-MachineConfig.ps1 b/Modules/Alkami.PowerShell.Common/Public/Read-MachineConfig.ps1 new file mode 100644 index 0000000..3b6ba3f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Read-MachineConfig.ps1 @@ -0,0 +1,20 @@ +function Read-MachineConfig { +<# +.SYNOPSIS + Returns the machine.config content as as an XML document +#> + + [CmdletBinding()] + Param( + [bool]$use64Bit = $true + ) + + $logLead = (Get-LogLeadName); + + $machineConfigPath = Get-DotNetConfigPath $use64Bit + Write-Verbose ("$logLead : Reading machine.config from {0}" -f $machineConfigPath) + [XML]$machineConfig = Get-Content $machineConfigPath + + return $machineConfig +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.ps1 b/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.ps1 new file mode 100644 index 0000000..60eb7d0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.ps1 @@ -0,0 +1,33 @@ +function Read-XMLFile { +<# +.SYNOPSIS + Reads a File and Returns the Content as XML + Returns $null if file cannot be converted to System.Xml.XmlDocument +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("FilePath")] + [string]$xmlPath + ) + + $logLead = (Get-LogLeadName); + Write-Verbose ("$logLead : Looking for XML file at {0}" -f $xmlPath) + + if (!(Test-Path $xmlPath)) { + Write-Warning ("$logLead : File could not be found at {0}" -f $xmlPath) + return $null + } + Write-Verbose ("$logLead : Reading XML File from {0}" -f $xmlPath) + try { + [Xml]$xmlContent = Get-Content $xmlPath -ErrorAction SilentlyContinue + } + catch { + Write-Warning ("$logLead : The Content of the Specified File is Null or Could Not be Cast to XML") + $xmlContent = $null + } + + return $xmlContent +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.tests.ps1 new file mode 100644 index 0000000..6cadb1a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Read-XMLFile.tests.ps1 @@ -0,0 +1,70 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +#region Read-XMLFile + +Describe "Read-XMLFile" { + + $tempPath = [System.IO.Path]::GetTempFileName() + + Context "When The File Does Not Exist" { + + if (Test-Path $tempPath) { + Remove-Item $tempPath -Force + } + + It "Returns Null" { + + Read-XMLFile $tempPath | Should Be $null + } + + It "Writes a Warning" { + + { + ((Read-XMLFile $tempPath) 3>&1) -match "File could not be found" + } | Should Be $true + } + } + + Context "When the File is Empty" { + + "" | Out-File $tempPath -Force -NoNewline + + It "Returns Null" { + + Read-XMLFile $tempPath | Should Be $null + } + + It "Writes a Warning" { + + { + ((Read-XMLFile $tempPath) 3>&1) -match "The Content of the Specified File is Null" + } | Should Be $true + } + } + + Context "When the File is Invalid XML" { + + "Hello World" | Out-File $tempPath -Force -NoNewline + + It "Returns Null" { + + Read-XMLFile $tempPath | Should Be $null + } + + It "Writes a Warning" { + + { + ((Read-XMLFile $tempPath) 3>&1) -match "Could Not be Cast to XML" + } | Should Be $true + } + } +} + +#endregion Read-XMLFile \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-DotNetTemporaryFiles.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-DotNetTemporaryFiles.ps1 new file mode 100644 index 0000000..c3edd53 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-DotNetTemporaryFiles.ps1 @@ -0,0 +1,32 @@ +function Remove-DotNetTemporaryFiles { +<# +.SYNOPSIS + Deletes .NET Temporary Files +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("AppPoolName")] + [string]$Name = "" + ) + + $logLead = (Get-LogLeadName); + + Write-Output ("$logLead : Deleting ASP.NET Temporary files"); + + $targetItems = Get-ChildItem "C:\Windows\Microsoft.NET\Framework*\v*\Temporary ASP.NET Files\$Name*"; + + foreach ($targetItem in $targetItems) + { + try + { + Write-Verbose ("$logLead : Removing {0}" -f $targetItem.FullName); + Remove-Item -Path $targetItem.FullName -Recurse -Force -ErrorAction SilentlyContinue; + } + catch + { + Write-Warning ("$logLead : Unable to remove file {0} - {1}" -f $targetItem.FullName, $_.Exception.Message); + } + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.Tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.Tests.ps1 new file mode 100644 index 0000000..830bd17 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.Tests.ps1 @@ -0,0 +1,106 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force + +Describe "Remove-FileSystemItem" { + ## https://pester.dev/docs/usage/testdrive/ + $existingOrbPath = (Join-Path $TestDrive orb) + + $garbageFileToBeGone = (Join-Path $existingOrbPath "letmebegone.txt") + $log4netFileToBeRemain = (Join-Path $existingOrbPath "log4net.config") + $symlinkActualFolder = (Join-Path $TestDrive "SymlinkSource") + $symlinkTargetFolder = (Join-Path $existingOrbPath "SymlinkTarget") + $symlinkDeepTargetFolder = (Join-Path $symlinkTargetFolder "DeepFolder") + $symlinkActualFile = (Join-Path $symlinkActualFolder "symlinkedfile.txt") + + Context "Basic workflow test with no confirmation level" { + ## Suppress Prompting + $ConfirmPreference = [System.Management.Automation.ConfirmImpact]::None + + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + + Remove-FileSystemItem $existingOrbPath -Recurse + + It "The file should not be there, the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + } + + Context "Basic workflow test with force" { + ##ensure our test files exist as expected. + + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + + Remove-FileSystemItem $existingOrbPath -Force + + It "The file should not be there, the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + } + + Context "Leaves log4net.config workflow test" { + ##ensure our test files exist as expected. + New-Item -Path $garbageFileToBeGone -ItemType File -Value "I should be gone after this test" -Force + New-Item -Path $log4netFileToBeRemain -ItemType File -Value "I should stick around" -Force + + Remove-FileSystemItem $existingOrbPath -Force -Exclude "log4net.config" + + It "The log4net file should not be deleted" { + (Test-Path $log4netFileToBeRemain) | Should -Be $true + Write-Debug (Get-Content $log4netFileToBeRemain) + } + + It "The file should not be there, the contents of the folder should have been deleted" { + (Test-Path $garbageFileToBeGone) | Should -Be $false + } + } + + Context "Removes Symlinked files test" { + New-Item -Path $symlinkActualFolder -ItemType Directory -Force + New-Item -Path $symlinkActualFile -ItemType File -Value "I should be gone after this test" -Force + $symlinkTargetFile = (Join-Path $symlinkTargetFolder "symlinkedfile.txt") + + New-Symlink -Path $symlinkActualFile -Destination $symlinkTargetFolder + Remove-FileSystemItem $symlinkTargetFile -Force + + It "The symlinked file should be gone but the actual file still present" { + (Test-Path $symlinkTargetFile) | Should -Be $false + (Test-Path $symlinkActualFile) | Should -Be $true + } + + } + + Context "Removes Symlinked folders test" { + New-Item -Path $symlinkActualFolder -ItemType Directory -Force + New-Item -Path $symlinkActualFile -ItemType File -Value "I should be gone after this test" -Force + + New-Symlink -Path $symlinkActualFolder -Destination $symlinkTargetFolder + Remove-FileSystemItem $symlinkTargetFolder -Force + + It "The symlinked folder should be gone but the actual file still present" { + (Test-Path $symlinkTargetFolder) | Should -Be $false + (Test-Path $symlinkActualFolder) | Should -Be $true + (Test-Path $symlinkActualFile) | Should -Be $true + } + } + + Context "Removes Deep Symlinked folders recursion test" { + New-Item -Path $symlinkActualFolder -ItemType Directory -Force + New-Item -Path $symlinkActualFile -ItemType File -Value "I should be gone after this test" -Force + + New-Symlink -Path $symlinkActualFolder -Destination $symlinkDeepTargetFolder + Remove-FileSystemItem $symlinkTargetFolder -Force + + It "The symlinked folder should be gone but the actual file still present" { + (Test-Path $symlinkTargetFolder) | Should -Be $false + (Test-Path $symlinkDeepTargetFolder) | Should -Be $false + (Test-Path $symlinkActualFolder) | Should -Be $true + (Test-Path $symlinkActualFile) | Should -Be $true + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.ps1 new file mode 100644 index 0000000..204f364 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-FileSystemItem.ps1 @@ -0,0 +1,258 @@ +function Remove-FileSystemItem { +<# +.SYNOPSIS +Removes files or directories reliably and synchronously. + +.DESCRIPTION +Removes files and directories, ensuring reliable and synchronous +behavior across all supported platforms. + +The syntax is a subset of what Remove-Item supports; notably -Include is NOT supported; + However, -Force and -Exclude are rudimentarily supported. + +As with Remove-Item, passing -Recurse is required to avoid a prompt when +deleting a non-empty directory. + +IMPORTANT: + * On Unix platforms, this function is merely a wrapper for Remove-Item, + where the latter works reliably and synchronously, but on Windows a + custom implementation must be used to ensure reliable and synchronous + behavior. See https://github.com/PowerShell/PowerShell/issues/8211 + +* On Windows: + * The *parent directory* of a directory being removed must be + *writable* for the synchronous custom implementation to work. + * The custom implementation is also applied when deleting + directories on *network drives*. + +* If an indefinitely *locked* file or directory is encountered, removal is aborted. + By contrast, files opened with FILE_SHARE_DELETE / + [System.IO.FileShare]::Delete on Windows do NOT prevent removal, + though they do live on under a temporary name in the parent directory + until the last handle to them is closed. + +* Hidden files and files with the read-only attribute: + * These are *quietly removed*; in other words: this function invariably + behaves like `Remove-Item -Force`. + * Note, however, that in order to target hidden files / directories + as *input*, you must specify them as a *literal* path, because they + won't be found via a wildcard expression. + +* The reliable custom implementation on Windows comes at the cost of + decreased performance. + +.PARAMETER Path + [string] This is the path to the item being deleted. Can be a file or folder. + +.PARAMETER LiteralPath + [string] Used to identify items that may be hidden. Can be a file or folder. + +.PARAMETER Exclude + [string] A pattern to match against using the naive powershell -match operator on the full item path + +.PARAMETER Force + [switch] Avoid prompting, the function already tries to force-delete even without this flag. + +.PARAMETER Recurse + [switch] Try to delete everything in a folder + +.PARAMETER SkipSymlinks + [switch] Skip deleting symlinks if specified + +.EXAMPLE +Remove-FileSystemItem C:\tmp -Recurse + +Synchronously removes directory C:\tmp and all its content. +#> + <#https://stackoverflow.com/questions/53553729/cannot-remove-item-the-directory-is-not-empty/53561052#53561052#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidShouldContinueWithoutForce", '', Justification="It's okay to use force to pass continue", Scope = "Function")] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Path', PositionalBinding = $false)] + param( + [Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string[]] $Path, + [Parameter(ParameterSetName = 'Literalpath', ValueFromPipelineByPropertyName)] + [Alias('PSPath')] + [string[]] $LiteralPath, + [string[]] $Exclude, + [switch] $Recurse, + [switch] $Force, + [switch] $SkipSymlinks + ) + begin { + $logLead = (Get-LogLeadName) + + if ($Force) { + ## Force recursion when told to Force delete + $Recurse = $true + } + + $script:excludePresent = ![string]::IsNullOrWhiteSpace($Exclude) + $script:excludedFilesFound = $false + $script:excludeMatch = $Exclude + # !! Workaround for https://github.com/PowerShell/PowerShell/issues/1759 + if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Ignore) { $ErrorActionPreference = 'Ignore'} + $targetPath = '' + $yesToAll = $noToAll = $false + function trimTrailingPathSep([string] $itemPath) { + if ($itemPath[-1] -in '\', '/') { + # Trim the trailing separator, unless the path is a root path such as '/' or 'c:\' + if ($itemPath.Length -gt 1 -and $itemPath -notmatch '^[^:\\/]+:.$') { + $itemPath = $itemPath.Substring(0, $itemPath.Length - 1) + } + } + $itemPath + } + function getTempPathOnSameVolume([string] $itemPath, [string] $tempDir) { + if (-not $tempDir) { $tempDir = [IO.Path]::GetDirectoryName($itemPath) } + [IO.Path]::Combine($tempDir, [IO.Path]::GetRandomFileName()) + } + function syncRemoveFile([string] $filePath, [string] $tempDir, [switch]$skipSymlink) { + if ($script:excludePresent -and ($filePath -match $script:excludeMatch)) { + $script:excludedFilesFound = $true + Write-Verbose "$logLead : Skipping file due to exclude match : [$filePath] | [$script:excludeMatch]" + return + } + + $attribs = [IO.File]::GetAttributes($filePath) + $isSymlink = ($attribs -band [System.IO.FileAttributes]::ReparsePoint) + if ($isSymlink -and $skipSymlink) { + # We don't mess with symlinked files here + # We do, however, review that we touched them so we can not-delete folders + $script:excludedFilesFound = $true + } else { + # Clear the ReadOnly attribute, if present. + $isReadOnly = ($attribs -band [System.IO.FileAttributes]::ReadOnly) + if ($isReadOnly) { + [IO.File]::SetAttributes($filePath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly) + } + if ($isSymlink) { + # Symlinks delete much faster than files. + [IO.File]::Delete($filePath) + } else { + # Faster disk IO if you move the files, then delete them, as file renames are _much_ faster to journaled file systems. + $tempPath = getTempPathOnSameVolume -itemPath $filePath -tempDir $tempDir + [IO.File]::Move($filePath, $tempPath) + [IO.File]::Delete($tempPath) + } + } + } + function syncRemoveDir([string] $dirPath, [switch] $recursing, [switch]$skipSymlink) { + if (-not $recursing) { $dirPathParent = [IO.Path]::GetDirectoryName($dirPath) } + # If the path is a Symlink, remove it and return without recursing into the folder. + # This is so we don't remove ex: c:\orb\shared\ but instead just the link. + + # Note: [IO.File]::*Attributes() is also used for *directories*; [IO.Directory] doesn't have attribute-related methods. + ($attribs = [IO.File]::GetAttributes($dirPath)) + $isSymlink = ($attribs -band [System.IO.FileAttributes]::ReparsePoint) + $isReadOnly = ($attribs -band [System.IO.FileAttributes]::ReadOnly) + if($isSymlink) { + if($skipSymlink) { + # We don't mess with symlinked files here + # We do, however, review that we touched them so we can not-delete folders + $script:excludedFilesFound = $true + } else { + # Clear the ReadOnly attribute, if present. + if ($isReadOnly) { + [IO.File]::SetAttributes($dirPath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly) + } + # Remove the actual folder item + [IO.Directory]::Delete($dirPath) + } + # leave the current subfunction stack + return + } + + # Clear the ReadOnly attribute, if present. + if ($isReadOnly) { + [IO.File]::SetAttributes($dirPath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly) + } + # Remove all children synchronously. + $isFirstChild = $true + foreach ($item in [IO.directory]::EnumerateFileSystemEntries($dirPath)) { + if (-not $recursing -and -not $Recurse -and $isFirstChild) { # If -Recurse wasn't specified, prompt for nonempty dirs. + $isFirstChild = $false + ## If you force the delete, don't even bother to prompt for confirmation + if (!$Force) { + # Note: If -Confirm was also passed, this prompt is displayed *in addition*, after the standard $PSCmdlet.ShouldProcess() prompt. + # While Remove-Item also prompts twice in this scenario, it shows the has-children prompt *first*. + $continuePrompt = "The item at '$dirPath' has children and the -Recurse switch was not specified. If you continue, all children will be removed with the item. Are you sure you want to continue?" + $shouldContinue = $PSCmdlet.ShouldContinue($continuePrompt, 'Confirm', ([ref] $yesToAll), ([ref] $noToAll)) + if (!$shouldContinue) { return } + } + } + + $itemPath = [IO.Path]::Combine($dirPath, $item) + ([ref] $targetPath).Value = $itemPath + + if ([IO.Directory]::Exists($itemPath)) { + syncremoveDir -dirPath $itemPath -recursing -skipSymlink:$skipSymlink + } else { + syncremoveFile -filePath $itemPath -tempDir $dirPathParent -skipSymlink:$skipSymlink + } + } + if (!$script:excludedFilesFound) { + Write-Verbose "$loglead : ExcludedFilesFound? [$script:excludedFilesFound]" + Write-Verbose "$loglead : Removing folder [$dirPath]" + # Finally, remove the directory itself synchronously if we didn't match a pattern on the input + ([ref] $targetPath).Value = $dirPath + $tempPath = (getTempPathOnSameVolume -itemPath $dirPath -tempDir $dirPathParent) + [IO.Directory]::Move($dirPath, $tempPath) + [IO.Directory]::Delete($tempPath) + } + } + } + + process { + $isLiteral = $PSCmdlet.ParameterSetName -eq 'LiteralPath' + Write-Verbose "$loglead : Skip Symlimks? [$SkipSymlinks]" + if ($env:OS -ne 'Windows_NT') { # Unix: simply pass through to Remove-Item, which on Unix works reliably and synchronously + Remove-Item @PSBoundParameters + } else { # Windows: use synchronous custom implementation + foreach ($rawPath in ($Path, $LiteralPath)[$isLiteral]) { + # Resolve the paths to full, filesystem-native paths. + try { + # !! Convert-Path does find hidden items via *literal* paths, but not via *wildcards* - and it has no -Force switch (yet) + # !! See https://github.com/PowerShell/PowerShell/issues/6501 + $resolvedPaths = if ($isLiteral) { Convert-Path -ErrorAction Stop -LiteralPath $rawPath } else { Convert-Path -ErrorAction Stop -path $rawPath} + } catch { + Write-Warning "$logLead : Could not delete file. More details follow." + Write-Error $_ # relay error, but in the name of this function + continue + } + try { + $isDir = $false + foreach ($resolvedPath in $resolvedPaths) { + # -WhatIf and -Confirm support. + if (-not $PSCmdlet.ShouldProcess($resolvedPath)) { continue } + if ($isDir = [IO.Directory]::Exists($resolvedPath)) { # dir. + # !! A trailing '\' or '/' causes directory removal to fail ("in use"), so we trim it first. + syncRemoveDir -dirPath (trimTrailingPathSep $resolvedPath) -skipSymlink:$SkipSymlinks + } elseif ([IO.File]::Exists($resolvedPath)) { # file + syncRemoveFile -filePath $resolvedPath -skipSymlink:$SkipSymlinks + } else { + if ($Force) { + Write-Warning "$logLead : Not a file-system path or no longer extant: $resolvedPath - was it deleted before now?" + } else { + throw "$logLead : Not a file-system path or no longer extant: $resolvedPath" + } + } + } + } catch { + if ($isDir) { + $exc = $_.Exception + if ($exc.InnerException) { $exc = $exc.InnerException } + if ($targetPath -eq $resolvedPath) { + Write-Error "$logLead : Removal of directory '$resolvedPath' failed: $exc" + } else { + Write-Error "$logLead : Removal of directory '$resolvedPath' failed, because its content could not be (fully) removed: $targetPath`: $exc" + } + } else { + Write-Warning "$logLead : Could not delete file. More details follow." + Write-Error $_ # relay error, but in the name of this function + } + continue + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.ps1 new file mode 100644 index 0000000..b4da3ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.ps1 @@ -0,0 +1,82 @@ +function Remove-ORBLogFiles { +<# +.SYNOPSIS + Deletes current ORB Logs in a folder + +.DESCRIPTION + In a given directory path, this will delete either all log files or all rolled log files. + +.PARAMETER LogDirectory + String representation of a path to a folder. Any log files in this folder could be deleted. + +.PARAMETER SkipActiveLogs + Switch to ignore any currently active logs (ones that have not rolled) +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("LogPath")] + [string]$LogDirectory, + + [Parameter(Mandatory = $false)] + [Alias("SkipActive")] + [switch]$SkipActiveLogs + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrEmpty($LogDirectory)) { + $LogDirectory = (Get-OrbLogsPath) + } + + $filter = if ($SkipActiveLogs) { "*.log.*" } else { "*.log*" }; + + # Remove any potential trailing wildcarded directories + while ($LogDirectory.EndsWith("\*") ) { + Write-Host "$logLead : Found a wildcard. Trimming..." + $LogDirectory = $LogDirectory.Substring(0, $LogDirectory.Length - 2) + } + + # Tack on a single \* so that GCI plays nicely with both the Include and Exclude parameters + $LogDirectory = $LogDirectory + "\*" + + # Force the array in case Get-ChildItem only returns one item + $logFiles = @() + $logFiles = [array](Get-ChildItem -Path $LogDirectory -Include $filter -File).Where({$_.Name -notmatch "\d{12}"}) + + # Slog files get saved with the date stamp in the filename + # For example: Alkami.Services.BillPayOrchestration-20210901.slog + # Super annoying + # Slog files also have file locks unless they've been written to, even if the date has rolled + # This should handle most cases + if ($SkipActiveLogs) { + $slogDateFormat = (Get-Date).ToString("yyyyMMdd") + $logFiles += [array](Get-ChildItem $logDirectory -Include "*.slog*" -Exclude "*$slogDateFormat.slog*" -File) -notmatch "\.(gz|7z|zip)" + } else { + + $logFiles += [array](Get-ChildItem $logDirectory -Include "*.slog*" -File) -notmatch "\.(gz|7z|zip)" + } + + if(Test-IsCollectionNullOrEmpty $logFiles) { + Write-Warning "$logLead : No log files to remove. Returning." + return; + } + + Write-Host ("$logLead : Delete {1} {0} files" -f $filter, $logFiles.Count) + + $errorCount = 0 + foreach ($logFile in $logFiles) { + try { + if ((Test-Path -Path $logFile) -eq $false) { + Write-Warning "$loglead : File does not exist: $logFile" + continue + } + Write-Verbose ("$logLead : Removing log file {0}" -f $logFile.FullName) + Remove-Item -Path $logFile.FullName -Force + } catch { + Write-Warning $_.Exception + $errorCount++ + } + } + Write-Host ("$logLead : {0} of {1} Deletes Complete" -f ($logFiles.Count - $errorCount), $logFiles.Count) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.tests.ps1 new file mode 100644 index 0000000..f3a2af3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-ORBLogFiles.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$FAKE_ORBLOGS = "TestDrive:\OrbLogs" +$FAKE_NOT_ORBLOGS = "TestDrive:\NotOrbLogs" + +Describe "Remove-ORBLogFiles" { + New-Item -Path $FAKE_ORBLOGS -ItemType Directory + New-Item -Path $FAKE_NOT_ORBLOGS -ItemType Directory + # CODEPATH: ALWAYS + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {return "[UnitTest_Default]"} + + # CODEPATH: No LogDirectory param + Mock -ModuleName $moduleForMock -CommandName Get-OrbLogsPath -MockWith {return $FAKE_ORBLOGS} + + # MUFFLER + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Remove-Item -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith {} + + Context "Parameters" { + It "Get_LogLead" { + + Remove-ORBLogFiles + + Assert-MockCalled -CommandName Get-LogLeadName -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + } + It "Get_GetOrbLogsPath_If_LogDirectory_Omitted" { + + Remove-ORBLogFiles + + Assert-MockCalled -CommandName Get-OrbLogsPath -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + } + It "Skip_GetOrbLogsPath_If_LogDirectory_Passed" { + + Remove-ORBLogFiles -LogDirectory $FAKE_NOT_ORBLOGS + + Assert-MockCalled -CommandName Get-OrbLogsPath -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + } + + } + Context "Log files path and remove tests" { + + It "TestPath is called" { + + Remove-ORBLogFiles -LogDirectory $FAKE_NOT_ORBLOGS + + Assert-MockCalled -CommandName Test-Path -ModuleName $moduleForMock -Scope It ` + -Times 1 -Exactly + } + It "Remove-Item isn't called" { + + Remove-ORBLogFiles -LogDirectory $FAKE_NOT_ORBLOGS + + Assert-MockCalled -CommandName Remove-Item -ModuleName $moduleForMock -Scope It ` + -Times 0 -Exactly + } + } + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Remove-OldArchivedLogFiles.ps1 b/Modules/Alkami.PowerShell.Common/Public/Remove-OldArchivedLogFiles.ps1 new file mode 100644 index 0000000..7ff199d --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Remove-OldArchivedLogFiles.ps1 @@ -0,0 +1,64 @@ +function Remove-OldArchivedLogFiles { +<# +.SYNOPSIS + Deletes Archived Log Files Older than a Number of Days in the Past +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("ArchivePath")] + [string]$archiveDirectory, + + [Parameter(Mandatory=$false)] + [Alias("CutoffThreshold")] + [int]$cutoffDays = 1 + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrEmpty($archiveDirectory)) { + $archiveDirectory = (Join-Path (Get-OrbLogsPath) "Archive") + } + + $absoluteCutoff = [System.Math]::Abs($cutoffDays) + $cutoffDate = (Get-Date).AddDays($absoluteCutoff * -1) + $cutoffUtc = [System.TimeZoneInfo]::ConvertTimeToUtc($cutoffDate) + + if (!(Test-Path $archiveDirectory)) { + (New-Item -ItemType Directory -Path $archiveDirectory -Force) | Out-Null + } + + $archivedFolders = Get-ChildItem $archiveDirectory -Directory + + Write-Output ("$logLead : Looking for old files to delete") + foreach ($archivedFolder in $archivedFolders) + { + try + { + $itemsToRemove = Get-ChildItem $archivedFolder.FullName -File | Where-Object { $_.CreationTimeUtc -lt $cutoffUtc } + $itemsToRemove | ForEach-Object { + + Write-Verbose ("$logLead : Removing Old Item {0}" -f $_.FullName) + Remove-Item $_.FullName -Force + } + } + catch + { + Write-Warning ("$logLead : An error occured while trying to evaluate/delete old log files: {0}" -f $_.Exception.Message) + } + } + + Write-Output ("$logLead : Looking for empty folders to delete") + try + { + Get-ChildItem $archiveDirectory -Recurse -Directory | Where-Object { $_.GetFiles().Count -eq 0 } | ForEach-Object { + + Write-Verbose ("$logLead : Removing Empty Folder {0}" -f $_.FullName) + Remove-Item $_.FullName -Recurse -Force + } + } + catch + { + Write-Warning ("$logLead : An error occured while trying to evaluate/delete empty folders: {0}" -f $_.Exception.Message) + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.ps1 b/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.ps1 new file mode 100644 index 0000000..4eb4c8a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.ps1 @@ -0,0 +1,250 @@ +Function Resolve-Error { + <# + .SYNOPSIS + Enumerate error record details. + + .DESCRIPTION + Enumerate an error record, or a collection of error record, properties. By default, the details + for the last error will be enumerated. + + .PARAMETER ErrorRecord + The error record to resolve. The default error record is the lastest one: $global:Error[0]. + This parameter will also accept an array of error records. + + .PARAMETER Property + The list of properties to display from the error record. Use "*" to display all properties. + Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException + + Below is a list of all of the possible available properties on the error record: + + Error Record: Error Invocation: Error Exception: Error Inner Exception(s): + $_ $_.InvocationInfo $_.Exception $_.Exception.InnerException + ------------- ----------------- ---------------- --------------------------- + writeErrorStream MyCommand ErrorRecord Data + PSMessageDetails BoundParameters ItemName HelpLink + Exception UnboundArguments SessionStateCategory HResult + TargetObject ScriptLineNumber StackTrace InnerException + CategoryInfo OffsetInLine WasThrownFromThrowStatement Message + FullyQualifiedErrorId HistoryId Message Source + ErrorDetails ScriptName Data StackTrace + InvocationInfo Line InnerException TargetSite + ScriptStackTrace PositionMessage TargetSite + PipelineIterationInfo PSScriptRoot HelpLink + PSCommandPath Source + InvocationName HResult + PipelineLength + PipelinePosition + ExpectingInput + CommandOrigin + DisplayScriptPosition + + .PARAMETER GetErrorRecord + Get error record details as represented by $_ + Default is to display details. To skip details, specify -GetErrorRecord $false + + .PARAMETER GetErrorInvocation + Get error record invocation information as represented by $_.InvocationInfo + Default is to display details. To skip details, specify -GetErrorInvocation $false + + .PARAMETER GetErrorException + Get error record exception details as represented by $_.Exception + Default is to display details. To skip details, specify -GetErrorException $false + + .PARAMETER GetErrorInnerException + Get error record inner exception details as represented by $_.Exception.InnerException. + Will retrieve all inner exceptions if there is more then one. + Default is to display details. To skip details, specify -GetErrorInnerException $false + + .EXAMPLE + Resolve-Error + + Get the default error details for the last error + + .EXAMPLE + Resolve-Error -ErrorRecord $global:Error[0,1] + + Get the default error details for the last two errors + + .EXAMPLE + Resolve-Error -Property * + + Get all of the error details for the last error + + .EXAMPLE + Resolve-Error -Property InnerException + + Get the "InnerException" for the last error + + .EXAMPLE + Resolve-Error -GetErrorInvocation $false + + Get the default error details for the last error but exclude the error invocation information + + .NOTES + Borrowed from https://stackoverflow.com/questions/795751/can-i-get-detailed-exception-stacktrace-in-powershell + Please return when done. + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] + [ValidateNotNullorEmpty()] + [array]$ErrorRecord, + + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'), + + [Parameter(Mandatory=$false)] + [bool]$GetErrorRecord = $true, + + [Parameter(Mandatory=$false)] + [bool]$GetErrorInvocation = $true, + + [Parameter(Mandatory=$false)] + [bool]$GetErrorException = $true, + + [Parameter(Mandatory=$false)] + [bool]$GetErrorInnerException = $true + ) + + Begin { + $logLead = (Get-LogLeadName) + + ## If function was called without specifying an error record, then choose the latest error that occured + if (-not $ErrorRecord) { + if ($global:Error.Count -eq 0) { + # The `$Error collection is empty + Write-Verbose "$logLead : global Error is empty." + return + } else { + Write-Verbose "$logLead : found items in global Error collection. Getting first error." + [array]$ErrorRecord = $global:Error[0] + } + } + + ## Define script block for selecting and filtering the properties on the error object + [scriptblock]$selectProperty = { + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + $InputObject, + + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string[]]$SelectedProperty + ) + + Write-Verbose "$logLead : InputObject [$InputObject]" + Write-Verbose "$logLead : SelectedProperty [$SelectedProperty]" + [string[]]$objectProperty = $InputObject | Get-Member -MemberType *Property | Select-Object -ExpandProperty Name + + foreach ($prop in $SelectedProperty) { + if ($prop -eq '*') { + [string[]]$propertySelection = $objectProperty + break + } + elseif ($objectProperty -contains $prop) { + [string[]]$propertySelection += $prop + } + } + return $propertySelection + } + + # Initialize variables to avoid error if 'Set-StrictMode' is set + $logErrorRecordMsg = $null + $logErrorInvocationMsg = $null + $logErrorExceptionMsg = $null + $logErrorMessageTmp = $null + $logInnerMessage = $null + } Process { + foreach ($errRecord in $ErrorRecord) { + ## Capture Error Record + if ($GetErrorRecord) { + Write-Verbose "$logLead : Capture Error Record" + [string[]]$selectedProperties = Invoke-Command -ScriptBlock $selectProperty -ArgumentList $errRecord, $Property + $logErrorRecordMsg = $errRecord | Select-Object -Property $selectedProperties + } + + ## Error Invocation Information + if ($GetErrorInvocation) { + if ($errRecord.InvocationInfo) { + Write-Verbose "$logLead : Get Error Invocation Information" + [string[]]$selectedProperties = Invoke-Command -ScriptBlock $selectProperty -ArgumentList $errRecord.InvocationInfo, $Property + $logErrorInvocationMsg = $errRecord.InvocationInfo | Select-Object -Property $selectedProperties + } + } + + ## Capture Error Exception + if ($GetErrorException) { + if ($errRecord.Exception) { + Write-Verbose "$logLead : Capture Error Exception" + [string[]]$selectedProperties = Invoke-Command -ScriptBlock $selectProperty -ArgumentList $errRecord.Exception, $Property + $logErrorExceptionMsg = $errRecord.Exception | Select-Object -Property $selectedProperties + } + } + + ## Display properties in the correct order + if ($Property -eq '*') { + # If all properties were chosen for display, then arrange them in the order + # the error object displays them by default. + Write-Verbose "$logLead : Display all properties" + if ($logErrorRecordMsg) {[array]$logErrorMessageTmp += $logErrorRecordMsg } + if ($logErrorInvocationMsg) {[array]$logErrorMessageTmp += $logErrorInvocationMsg} + if ($logErrorExceptionMsg) {[array]$logErrorMessageTmp += $logErrorExceptionMsg } + } else { + # Display selected properties in our custom order + Write-Verbose "$logLead : Display selected properties in custom order" + if ($logErrorExceptionMsg) {[array]$logErrorMessageTmp += $logErrorExceptionMsg } + if ($logErrorRecordMsg) {[array]$logErrorMessageTmp += $logErrorRecordMsg } + if ($logErrorInvocationMsg) {[array]$logErrorMessageTmp += $logErrorInvocationMsg} + } + + if ($logErrorMessageTmp) { + $logErrorMessage = 'Error Record:' + $logErrorMessage += "`n-------------" + $logErrorMsg = $logErrorMessageTmp | Format-List | Out-String + $logErrorMessage += $logErrorMsg + } + + ## Capture Error Inner Exception(s) + if ($GetErrorInnerException) { + if ($errRecord.Exception -and $errRecord.Exception.InnerException) { + Write-Verbose "$logLead : Capture Error Inner Exception" + $logInnerMessage = 'Error Inner Exception(s):' + $logInnerMessage += "`n-------------------------" + + $errorInnerException = $errRecord.Exception.InnerException + $Count = 0 + + while ($errorInnerException) { + $innerExceptionSeperator = '~' * 40 + + [string[]]$selectedProperties = Invoke-Command -ScriptBlock $selectProperty -ArgumentList $errorInnerException, $Property + $logErrorInnerExceptionMsg = $errorInnerException | Select-Object -Property $selectedProperties | Format-List | Out-String + + if ($Count -gt 0) { + $logInnerMessage += $innerExceptionSeperator + } + $logInnerMessage += $logErrorInnerExceptionMsg + + $Count++ + $errorInnerException = $errorInnerException.InnerException + } + } + } + + if ($logErrorMessage) { $output += $logErrorMessage } + if ($logInnerMessage) { $output += $logInnerMessage } + + Write-Verbose "$logLead : Write output" + Write-Host $output + + Write-Verbose "$logLead : Cleanup variables" + if (Test-Path -Path 'variable:Output' ) { Clear-Variable -Name output } + if (Test-Path -Path 'variable:LogErrorMessage' ) { Clear-Variable -Name logErrorMessage } + if (Test-Path -Path 'variable:logInnerMessage' ) { Clear-Variable -Name logInnerMessage } + if (Test-Path -Path 'variable:logErrorMessageTmp') { Clear-Variable -Name logErrorMessageTmp } + } + } + End {} +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.tests.ps1 new file mode 100644 index 0000000..8d7361e --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Resolve-Error.tests.ps1 @@ -0,0 +1,55 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Resolve-Error" { + + Context "Captures the error" { + + It "Writes to Output" { + + Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {} + + try { + throw "error" + } catch { + Resolve-Error + Assert-MockCalled -CommandName Write-Host -Scope It + } + } + } + + Context "Should not throw" { + + It "Parameter is throw safe" { + + $e = New-Object System.Exception + { Resolve-Error -ErrorRecord $e } | Should -not -Throw + } + + It "Parameter position is throw safe" { + + $e = New-Object System.Exception + { Resolve-Error $e } | Should -not -Throw + } + } + + Context "Should throw" { + + It "Null parameter should throw" { + + $e = $null + { Resolve-Error -ErrorRecord $e } | Should -Throw + } + + It "Null parameter position should throw" { + + $e = $null + { Resolve-Error $e } | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Revoke-LogonUsers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Revoke-LogonUsers.ps1 new file mode 100644 index 0000000..0c80357 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Revoke-LogonUsers.ps1 @@ -0,0 +1,79 @@ +function Revoke-LogonUsers { +<# + +.SYNOPSIS + Logs out all user sessions with an option to skip the current user + +.DESCRIPTION + This function is used to log out all remote or local sessions from a computer + If passed with -skipMe, the current user's session will not be terminated + +.PARAMETER skipMe + [switch] Skips the current user session. Optional. + +.EXAMPLE + Revoke-LogonUsers + +[Revoke-LogonUsers] : Logging Off Users: +USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME +sakbar 11 Disc 20:38 8/15/2019 9:07 PM +msatpathy 12 Disc 20:51 8/15/2019 9:15 PM +>dsage rdp-tcp#10 13 Active . 8/16/2019 4:45 PM +ccoane rdp-tcp#9 14 Active 19 8/16/2019 5:07 PM +Logging off session ID 11 +Logging off session ID 12 +Logging off session ID 13 +<> + +.EXAMPLE + Revoke-LogonUsers -skipMe + +[Revoke-LogonUsers] : Logging Off Users: +USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME +sakbar 11 Disc 20:38 8/15/2019 9:07 PM +msatpathy 12 Disc 20:51 8/15/2019 9:15 PM +>dsage rdp-tcp#10 13 Active . 8/16/2019 4:45 PM +ccoane rdp-tcp#9 14 Active 19 8/16/2019 5:07 PM +Logging off session ID 11 +Logging off session ID 12 +[Revoke-LogonUsers] : Skipping Current User Session +Logging off session ID 14 + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [switch]$skipMe + ) + + $logLead = (Get-LogLeadName); + + if (Test-IsAdmin) { + + $query = query user 2> $null + if($query) { + + Write-Host "$logLead : Logging Off Users:" + $query + + $users = query user | Select-Object -Skip 1 | Where-Object {($_ -split "\s+")[-5]} + + foreach ($userLine in $users) { + + if ($skipMe.IsPresent -and $userLine -match "$env:username") { + + Write-Host "$logLead : Skipping Current User Session" + continue; + } + + logoff ($userLine -split "\s+")[-6] /V + } + } else { + + Write-Host ("$logLead : No User Sessions Found") + } + } else { + + throw ("$logLead : Local Administrative Privileges are Required to Execute This Function") + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.ps1 b/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.ps1 new file mode 100644 index 0000000..159ba1c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.ps1 @@ -0,0 +1,75 @@ +function Save-XMLFile { +<# +.SYNOPSIS + Saves XML Content to File + +.NOTES + Default Encoding is UTF-8 +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("FilePath")] + [string]$XmlPath, + + [Parameter(Mandatory = $true)] + [Alias("XmlContent")] + [Xml]$Xml, + + [Parameter(Mandatory = $false)] + [Alias("Encoding")] + [System.Text.Encoding]$OutputEncoding = [System.Text.Encoding]::UTF8 + ) + + $logLead = Get-LogLeadName + + if ($null -eq (([System.Management.Automation.PSTypeName]'StringWriterWithEncoding').Type)) { + $encodingStringWriter = @" + using System.IO; + using System.Text; + public sealed class StringWriterWithEncoding : StringWriter + { + private readonly Encoding encoding; + + public StringWriterWithEncoding(Encoding encoding) + { + this.encoding = encoding; + } + + public override Encoding Encoding + { + get { return encoding; } + } + } +"@ + + Write-Verbose "$logLead : Adding Custom StringWriter" + Add-Type -ReferencedAssemblies @( "System.IO" ) -TypeDefinition $encodingStringWriter -Language CSharp + } + + Write-Verbose "$logLead : Creating StringWriterWithEncoding" + $stringWriter = New-Object StringWriterWithEncoding($OutputEncoding) + + Write-Verbose "$logLead : Importing System.XML" + [System.Reflection.Assembly]::LoadWithPartialName("System.XML") | Out-Null + + Write-Verbose "$logLead : Creating XmlWriterSettings" + $settings = New-Object System.XML.XmlWriterSettings + $settings.Indent = $true + + try { + Write-Verbose "$logLead : Creating XmlWriter" + $writer = [System.Xml.XmlWriter]::Create($stringWriter, $settings) + Write-Verbose "$logLead : Writing XML to Stream" + $Xml.Save($writer) + } finally { + if ($null -ne $writer) { + Write-Verbose "$logLead : Disposing XmlWriter" + $writer.Dispose() + } + } + + Write-Host ("$logLead : Saving XML to {0}" -f $XmlPath) + [System.IO.File]::WriteAllLines($XmlPath, $stringWriter.ToString(), $OutputEncoding) +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.tests.ps1 new file mode 100644 index 0000000..093b91a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Save-XMLFile.tests.ps1 @@ -0,0 +1,81 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +#region Save-XMLFile + +Describe Save-XMLFile { + + $tempPath = [System.IO.Path]::GetTempFileName() + $sampleXml = '' + $prettyXml = '' + + Context "When the File is Valid XML" { + + function Remove-TestXMLFile { + + if (Test-Path $tempPath) { + Remove-Item $tempPath -Force + } + } + + It "Saves the XML" { + + Remove-TestXMLFile + Save-XMLFile $tempPath $sampleXml + $resultingFile = Read-XmlFile $tempPath + $resultingFile.OuterXml.ToString() | Should Match "" + } + + It "Saves the XML Declaration Header" { + + Remove-TestXMLFile + Save-XMLFile $tempPath $sampleXml + $resultingFile = Get-Content $tempPath + $resultingFile[0] | Should Match '' + $resultingFile[2] | Should Match '' + $resultingFile.Count | Should Be 4 + } + + It "Saves the XML in UTF-8 with BOM by Default" { + + Remove-TestXMLFile + Save-XMLFile $tempPath $prettyXml + $resultingFile = Get-Content $tempPath + $resultingFile[0] | Should Match 'utf-8' + } + + It "Saves the XML in the Specified Encoding" { + + Remove-TestXMLFile + $encoding = [System.Text.Encoding]::ASCII + Save-XMLFile $tempPath $prettyXml $encoding + $resultingFile = Get-Content $tempPath + $resultingFile[0] | Should Match 'ascii' + } + } + + Context "When the String is Invalid XML" { + + It "Throws an Error" { + + $invalidXml = "Hello World!" + { Save-XMLFile $tempPath $invalidXml -ErrorAction SilentlyContinue } | Should Throw + } + } +} + +#endregion Save-XMLFile \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Search-ForRunningWorkerProcesses.ps1 b/Modules/Alkami.PowerShell.Common/Public/Search-ForRunningWorkerProcesses.ps1 new file mode 100644 index 0000000..68201ec --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Search-ForRunningWorkerProcesses.ps1 @@ -0,0 +1,26 @@ +function Search-ForRunningWorkerProcesses { +<# +.SYNOPSIS + Checks for running IIS, Nag, and Radium Processes +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : Checking for running processes") + $isProcessActive = Get-Process | Where-Object {$_.Name -match "(\.Nag|\.Radium|w3wp|Alkami)" -and $_.Name -notmatch "(Deconversion|Wintest|ServerManager)"} -ErrorAction SilentlyContinue + + if($null -eq $isProcessActive) + { + Write-Verbose ("$logLead : IIS, Radium, Nag, and MicroService Worker processes are done") + return $true + } + else + { + Write-Warning ("$logLead : Found Running Processes -- Execution Cannot Continue") + $isProcessActive | Select-Object Name, Id | Format-Table -Force + return $false + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.ps1 new file mode 100644 index 0000000..0a78d35 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.ps1 @@ -0,0 +1,19 @@ +function Select-AlkamiAppServers { +<# +.SYNOPSIS +Filters app tier App servers from a list of servers by hostname convention. +#> + [CmdletBinding()] + Param( + [AllowNull()] + [AllowEmptyCollection()] + [Parameter(Mandatory=$true)] + [string[]]$Servers + ) + + $result = ($Servers -match "\b(APP|ALK-PLA1-QA)") + if ($result -eq $false) { + return $null + } + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.tests.ps1 new file mode 100644 index 0000000..0ae35ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiAppServers.tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-AlkamiAppServers" { + + Context "Server Filter" { + + $servers = @("APP1", "APP2", "ALK-PLA1-QA4", "BadAPP123", "MIC1", "FAB1", "WEB1", "ALK-PLA1-QW4") + + It "Filters App Servers" { + $filtered = Select-AlkamiAppServers -Servers $servers + $filtered | Should -Be @("APP1", "APP2", "ALK-PLA1-QA4") + } + + It "Handles Empty Collection" { + $result = Select-AlkamiAppServers -Servers @() + $result | Should -Be $null + } + + It "Handles Null" { + $result = Select-AlkamiAppServers -Servers $null + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.ps1 new file mode 100644 index 0000000..29a7d32 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.ps1 @@ -0,0 +1,19 @@ +function Select-AlkamiFabServers { +<# +.SYNOPSIS +Filters Service Fabric FAB servers from a list of servers by hostname convention. +#> + [CmdletBinding()] + Param( + [AllowNull()] + [AllowEmptyCollection()] + [Parameter(Mandatory=$true)] + [string[]]$Servers + ) + + $result = ($Servers -match "\bFAB") + if ($result -eq $false) { + return $null + } + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.tests.ps1 new file mode 100644 index 0000000..fcb0c3b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiFabServers.tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-AlkamiFabServers" { + + Context "Server Filter" { + + $servers = @("FAB1", "FAB2", "BadFAB123", "ALK-PLA1-QA4", "MIC1", "APP1", "WEB1", "ALK-PLA1-QW4") + + It "Filters Fab Servers" { + $filtered = Select-AlkamiFabServers -Servers $servers + $filtered | Should -Be @("FAB1", "FAB2") + } + + It "Handles Empty Collection" { + $result = Select-AlkamiFabServers -Servers @() + $result | Should -Be $null + } + + It "Handles Null" { + $result = Select-AlkamiFabServers -Servers $null + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.ps1 new file mode 100644 index 0000000..90c5966 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.ps1 @@ -0,0 +1,19 @@ +function Select-AlkamiMicServers { +<# +.SYNOPSIS +Filters app tier Mic servers from a list of servers by hostname convention. +#> + [CmdletBinding()] + Param( + [AllowNull()] + [AllowEmptyCollection()] + [Parameter(Mandatory=$true)] + [string[]]$Servers + ) + + $result = ($Servers -match "\b(MIC|ALK-PLA1-QM)") + if ($result -eq $false) { + return $null + } + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.tests.ps1 new file mode 100644 index 0000000..0d1192a --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiMicServers.tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-AlkamiMicServers" { + + Context "Server Filter" { + + $servers = @("MIC1", "MIC2", "ALK-PLA1-QM4", "BadMIC123", "APP1", "FAB1", "WEB1", "ALK-PLA1-QW4") + + It "Filters Mic Servers" { + $filtered = Select-AlkamiMicServers -Servers $servers + $filtered | Should -Be @("MIC1", "MIC2", "ALK-PLA1-QM4") + } + + It "Handles Empty Collection" { + $result = Select-AlkamiMicServers -Servers @() + $result | Should -Be $null + } + + It "Handles Null" { + $result = Select-AlkamiMicServers -Servers $null + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.ps1 new file mode 100644 index 0000000..1c73fdb --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.ps1 @@ -0,0 +1,19 @@ +function Select-AlkamiTeaServers { + <# + .SYNOPSIS + Filters TeamCity servers from a list of servers by hostname convention. + #> + [CmdletBinding()] + Param( + [AllowNull()] + [AllowEmptyCollection()] + [Parameter(Mandatory = $true)] + [string[]]$Servers + ) + + $result = ($Servers -match "\bTEA") + if ($result -eq $false) { + return $null + } + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.tests.ps1 new file mode 100644 index 0000000..8e55945 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiTeaServers.tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-AlkamiTeaServers" { + + Context "Server Filter" { + + $servers = @("TEA1", "TEA2", "BadTEA123", "APP1", "FAB1", "WEB1", "MIC1") + + It "Filters TEA Servers" { + $filtered = Select-AlkamiTeaServers $servers + $filtered | Should -Be @("TEA1", "TEA2") + } + + It "Handles Empty Collection" { + $result = Select-AlkamiTeaServers -servers @() + $result | Should -Be $null + } + + It "Handles Null" { + $result = Select-AlkamiTeaServers -servers $null + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.ps1 new file mode 100644 index 0000000..48331fe --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.ps1 @@ -0,0 +1,19 @@ +function Select-AlkamiWebServers { +<# +.SYNOPSIS +Filters Web servers from a list of servers by hostname convention. +#> + [CmdletBinding()] + Param( + [AllowNull()] + [AllowEmptyCollection()] + [Parameter(Mandatory=$true)] + [string[]]$Servers + ) + + $result = ($Servers -match "\b(WEB|ALK-PLA1-QW)") + if ($result -eq $false) { + return $null + } + return $result +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.tests.ps1 new file mode 100644 index 0000000..d813dd4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Select-AlkamiWebServers.tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Select-AlkamiWebServers" { + + Context "Server Filter" { + + $servers = @("WEB1", "WEB2", "ALK-PLA1-QW4", "BadWEB1234", "MIC1", "FAB1", "APP1", "ALK-PLA1-QM4") + + It "Filters Web Servers" { + $filtered = Select-AlkamiWebServers $servers + $filtered | Should -Be @("WEB1", "WEB2", "ALK-PLA1-QW4") + } + + It "Handles Empty Collection" { + $result = Select-AlkamiWebServers -Servers @() + $result | Should -Be $null + } + + It "Handles Null" { + $result = Select-AlkamiWebServers -Servers $null + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Set-ConnectionString.ps1 b/Modules/Alkami.PowerShell.Common/Public/Set-ConnectionString.ps1 new file mode 100644 index 0000000..282778e --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Set-ConnectionString.ps1 @@ -0,0 +1,86 @@ +function Set-ConnectionString { +<# +.SYNOPSIS + Sets a connection string in the specified config file. Filepath defaults to the 64 bit machine config. +#> + param ( + [Parameter(Mandatory = $true)] + [string]$name, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$connectionString, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$filePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName); + + # If a computername was provided, modify the filepath to be a UNC path. + if((![string]::IsNullOrWhiteSpace($ComputerName)) -and ($ComputerName -ne "localhost")) { + $filePath = (Get-UncPath -filePath $filePath -ComputerName $ComputerName); + } + + if (!(Test-Path -PathType Leaf -Path $filePath)) { + Write-Warning ("$logLead : Could not find a file at {0}. Execution cannot continue" -f $filePath); + return $null; + } + + Write-Verbose "$logLead : Reading Config file at $filePath"; + $xml = Read-XMLFile $filePath; + if(!$xml) { + throw "$logLead : Config at $filePath could not be converted to xml."; + } + + Write-Verbose "$logLead : Ensuring configuration and connectionStrings nodes exist.."; + if(!$xml.configuration) { + [void]$xml.AppendChild($xml.CreateNode("element","configuration", $null)) + } + if(!$xml.configuration.connectionStrings) { + [void]$xml.SelectSingleNode("configuration").AppendChild($xml.CreateElement("connectionStrings")); + } + + Write-Verbose "$logLead : Looking for an existing `"$name`" connection string." + $connectionStrings = $xml.configuration.SelectSingleNode("connectionStrings"); + $foundSetting = $false; + $dirty = $false; + if($connectionStrings.SelectNodes("add").count -gt 0) { + $connectionNode = $connectionStrings.add | Where-Object { $_.Name -eq $name; } + if($connectionNode) { + $foundSetting = $true; + + if($connectionNode.connectionString -ne $connectionString) { + Write-Host "$logLead : Found appSetting `"$name`", changing value from `"$($connectionNode.connectionString)`" to `"$connectionString`"."; + $connectionNode.connectionString = $connectionString; + $dirty = $true; + } + } + } + + if(!$foundSetting) { + Write-Host "$logLead : Could not find connection string name `"$name`". Creating one and setting value to `"$connectionString`"."; + + $connectionStringElement = $xml.CreateElement("add"); + $connectionStringElement.SetAttribute("name", $name); + $connectionStringElement.SetAttribute("connectionString", $connectionString); + if($name -eq "AlkamiMaster") { + $connectionStringElement.SetAttribute("providerName", "System.Data.SqlClient"); + } + + [void]$connectionStrings.AppendChild($connectionStringElement); + + $dirty = $true; + } + + if($dirty) { + Write-Verbose "$logLead : Saving Config to path $filePath"; + $xml.Save($filePath); + } else { + Write-Verbose "$logLead : No changes were made to $filePath" + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Set-RegistryValue.ps1 b/Modules/Alkami.PowerShell.Common/Public/Set-RegistryValue.ps1 new file mode 100644 index 0000000..b788a5c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Set-RegistryValue.ps1 @@ -0,0 +1,25 @@ +function Set-RegistryValue { +<# +.SYNOPSIS + Sets a registry key to a specific value +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [string]$registryPath, + [string]$keyName, + [string]$desiredValue + ) + + $logLead = (Get-LogLeadName); + $itemProp = Get-ItemProperty -Path $registryPath -Name $keyName + + if ($itemProp.$keyName -eq $desiredValue) { + Write-Host ("$logLead : The $keyName value is already set to $desiredValue -- no changes required") + return $false + } + + Write-Host ("$logLead : Setting the $keyName Value to $desiredValue") + Set-ItemProperty -Path $registryPath -Name $keyName -Value $desiredValue + return $true +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Set-SystemWebSettings.ps1 b/Modules/Alkami.PowerShell.Common/Public/Set-SystemWebSettings.ps1 new file mode 100644 index 0000000..3d8eb3c --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Set-SystemWebSettings.ps1 @@ -0,0 +1,106 @@ +function Set-SystemWebSettings { +<# +.SYNOPSIS + Sets system.web settings to recommended values in the specified config file. Filepath defaults to the 64 bit machine config. +#> + param ( + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$filePath = (Get-DotNetConfigPath -use64Bit $true) + ) + $XmlNodeValues = @( + @{XPath = "//system.web/processModel"; AttributeName = "autoConfig"; AttributeValue = "false" }, + @{XPath = "//system.web/processModel"; AttributeName = "maxWorkerThreads"; AttributeValue = "400" }, + @{XPath = "//system.web/processModel"; AttributeName = "maxIoThreads"; AttributeValue = "400" }, + @{XPath = "//system.web/processModel"; AttributeName = "minWorkerThreads"; AttributeValue = "100" }, + @{XPath = "//system.web/processModel"; AttributeName = "minIoThreads"; AttributeValue = "100" }, + @{XPath = "//system.web/httpRuntime"; AttributeName = "minFreeThreads"; AttributeValue = "704" }, + @{XPath = "//system.web/httpRuntime"; AttributeName = "minLocalRequestFreeThreads"; AttributeValue = "608" } + ) + + $logLead = (Get-LogLeadName); + $dirty = $false; + + if (!(Test-Path -PathType Leaf -Path $filePath)) + { + Write-Warning ("$logLead : Could not find a file at {0}. Execution cannot continue" -f $filePath); + return $null; + } + + Write-Verbose "$logLead : Reading Config file at $filePath"; + $xml = Read-XMLFile $filePath; + if(!$xml) + { + throw "$logLead : Config at $filePath could not be converted to xml."; + } + + Write-Verbose "$logLead : Ensuring configuration and connectionStrings nodes exist..."; + if(!$xml.configuration){ + [void]$xml.AppendChild($xml.CreateNode("element","configuration", $null)) + $dirty = $true; + } else { + Write-Host "$logLead : Found configuration node" + } + if(!$xml.configuration.'system.web'){ + [void]$xml.SelectSingleNode("configuration").AppendChild($xml.CreateElement("system.web")); + $dirty = $true; + } else { + Write-Verbose "$logLead : Found system.web node" + } + + $systemWebNode = $xml.configuration.SelectSingleNode('system.web') + Write-Verbose "$logLead : Looking for an existing 'processModel' node." + $processModel = $xml.configuration.'system.web'.SelectSingleNode("processModel"); + $foundSetting = $false; + if($processModel) + { + $foundSetting = $true; + Write-Verbose "$logLead : Found processModel node."; + } + if(!$foundSetting) + { + Write-Host "$logLead : Could not find processModel node, creating one."; + $processModelElement = $xml.CreateElement("processModel"); + $systemWebNode.AppendChild($processModelElement); + $dirty = $true; + } + + $foundSetting = $false + Write-Verbose "$logLead : Looking for an existing 'httpRuntime' node." + $httpRuntime = $xml.configuration.'system.web'.SelectSingleNode("httpRuntime"); + if($httpRuntime) + { + $foundSetting = $true; + Write-Verbose "$logLead : Found httpRuntime node."; + } + if(!$foundSetting) + { + Write-Host "$logLead : Could not find httpRuntime node, creating one."; + $httpRuntimeElement = $xml.CreateElement("httpRuntime"); + $systemWebNode.AppendChild($httpRuntimeElement); + $dirty = $true; + } + + foreach ($node in $XmlNodeValues) { + Write-Host ("$logLead : Setting {0} to {1}" -f $node.XPath, $node.AttributeValue) + $changed = Set-XmlNodeValue $xml ` + -NodeString $node.XPath ` + -AttributeName $node.AttributeName ` + -AttributeValue $node.AttributeValue ` + -ErrorAction SilentlyContinue + if ($changed) { + $dirty = $true + } + } + + if($dirty) + { + # Global variable - $utfNoBOM is an Alkami global variable to make up for a PS shortcoming. + Write-Verbose "$logLead : Saving Config to path $filePath"; + Save-XMLFile $filePath $xml.OuterXml $utfNoBOM + } + else + { + Write-Verbose "$logLead : No changes were made to $filePath" + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.ps1 b/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.ps1 new file mode 100644 index 0000000..8ac644b --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.ps1 @@ -0,0 +1,36 @@ +function Set-XmlNodeValue { +<# +.SYNOPSIS + Set web.config node. Depend on Set-AlkamiConfigs +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][Xml]$doc, + [string]$NodeString, + [String]$AttributeName, + [String]$AttributeValue + ) + + $logLead = (Get-LogLeadName); + + $matchingNodes = $doc.SelectNodes($NodeString) + + if ($null -eq $matchingNodes -or $matchingNodes.Count -eq 0) + { + Write-Warning ("$logLead : Could not find node {0} in the supplied XML" -f $NodeString) + } + elseif ($matchingNodes.GetAttribute($AttributeName) -eq $AttributeValue) + { + Write-Host "$logLead : Node already has correct value '$AttributeValue'" + $changed = $false + } + else + { + Write-Output ("$logLead : Updating Node '{0}' With Value '{1}'" -f $NodeString, $AttributeValue) + $matchingNodes.SetAttribute($AttributeName,$AttributeValue) + $changed = $true + } + + Return $changed +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.tests.ps1 new file mode 100644 index 0000000..758b3ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Set-XmlNodeValue.tests.ps1 @@ -0,0 +1,46 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-XmlNodeValue + +Describe Set-XmlNodeValue { + + [xml]$fakeXml = @" + + + + + + + +"@ + + It "Updates a Node When Valid XPath is Provided" { + + Set-XmlNodeValue $fakeXml "//appSettings/add[@key='TestNode1']" -AttributeName "value" -AttributeValue "Updated" + $fakeXml.SelectSingleNode("//appSettings/add[@key='TestNode1']").Value -eq "Updated" | Should Be $true + } + + It "Writes a Warning When the Node Doesn't Exist" { + + { + ( Set-XmlNodeValue $fakeXml "//appSettings/add[@key='IDontExist']" -AttributeName "value" -AttributeValue "Updated" 3>&1 ) -match + "Could not find node" + } | Should Be $true + } + + It "Makes No Update Unless Needed" { + + { + ( Set-XmlNodeValue $fakeXml "//appSettings/add[@key='TestNode2']" -AttributeName "value" -AttributeValue "TestValue2" ) -match + "Node already has correct value 'TestValue2'" + } | Should Be $true + } +} + +#endregion Set-XmlNodeValue \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Stop-ProcessIfFound.ps1 b/Modules/Alkami.PowerShell.Common/Public/Stop-ProcessIfFound.ps1 new file mode 100644 index 0000000..035209f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Stop-ProcessIfFound.ps1 @@ -0,0 +1,27 @@ +function Stop-ProcessIfFound { +<# +.SYNOPSIS + Forcefully terminates a process if found +#> + param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateScript( {(!([String]::IsNullOrEmpty($_)))})] + [string]$processName + ) + + $logLead = (Get-LogLeadName); + + $processes = Get-Process -Name $processName -ErrorAction SilentlyContinue; + + Write-Verbose ("$logLead : Found {0} process(es) with name {1}" -f $processes.Count, $processName); + + if ($processes.Count -gt 0) { + Write-Output ("$logLead : Stopping {0} instance(s) of process {1}" -f $processes.Count, $processName); + + foreach ($process in ($processes | Where-Object {$null -ne $_.Id})) { + if ($null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue)) { + Stop-Process -InputObject $process -Force; + } + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-ComputerIsAvailable.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-ComputerIsAvailable.ps1 new file mode 100644 index 0000000..f24acdf --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-ComputerIsAvailable.ps1 @@ -0,0 +1,29 @@ +function Test-ComputerIsAvailable { +<# +.SYNOPSIS + Test that a given computer is on and able to be connected to + +.PARAMETER ComputerName + [string] Computer to connect to. Assumes .fh.local domain + +.PARAMETER NotFHLocal + [switch] Disable check to see if it is a .fh.local computer name +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [string]$ComputerName, + [switch]$NotFHLocal + ) + + try { + if (!$NotFHLocal -and !$ComputerName.EndsWith('.fh.local')) { + $ComputerName = "$ComputerName.fh.local" + } + $session = New-PSSession -ComputerName $ComputerName + Remove-PSSession $session + return $true + } catch { + return $false + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsAdmin.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsAdmin.ps1 new file mode 100644 index 0000000..4792b17 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsAdmin.ps1 @@ -0,0 +1,19 @@ +function Test-IsAdmin { +<# +.SYNOPSIS + Returns true if the current user is an administrator. +#> + [CmdletBinding()] + Param() + + try { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent(); + $principal = New-Object Security.Principal.WindowsPrincipal -ArgumentList $identity; + return $principal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ); + } + catch { + throw "Failed to determine if the current user has elevated privileges. The error was: '{0}'." -f $_; + } +} + +Set-Alias IsAdmin Test-IsAdmin -Force -Scope:Global diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsCollectionNullOrEmpty.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsCollectionNullOrEmpty.ps1 new file mode 100644 index 0000000..629e4d7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsCollectionNullOrEmpty.ps1 @@ -0,0 +1,20 @@ +Function Test-IsCollectionNullOrEmpty { +<# +.SYNOPSIS + Simple Null or Empty Filter +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$false, Position=0)] + $Collection + ) + + if ($null -eq $collection -or ([array]$collection).Count -eq 0) { + return $true + } + + return $false +} + +Set-Alias IsCollectionNullOrEmpty Test-IsCollectionNullOrEmpty -Force -Scope:Global \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.ps1 new file mode 100644 index 0000000..4155dee --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.ps1 @@ -0,0 +1,36 @@ +Function Test-IsLoadTestEnvironment { +<# +.SYNOPSIS + Returns true if the environment type is designated as for-sure LoadTest or if the environment name of the ComputerName machine matches the load test environment pattern + +.PARAMETER ComputerName + The machine to test. +#> + [CmdletBinding(DefaultParameterSetName = 'ByComputer')] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$false, ParameterSetName = "ByComputer")] + [string]$ComputerName = "localhost", + [Parameter(Mandatory=$false, ParameterSetName = "ByEnvironmentName")] + [string]$EnvironmentName = $null + + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('LoadTest') + + if ($environmentTypeNames -contains $environmentType) { + return $true + } + + if([string]::IsNullOrWhiteSpace($EnvironmentName)) { + # It's a load test environment if the environment name contains the whole word "vPod". + # Note: We can't trust the Environment.Type because it is set to Staging. + $EnvironmentName = Get-AppSetting -Key "Environment.Name" -ComputerName $ComputerName + } + + $isLoadTestEnvironment = ($EnvironmentName -match "\bvPod\b") -or ($EnvironmentName -match "ltvpod"); + + return $isLoadTestEnvironment +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.tests.ps1 new file mode 100644 index 0000000..9c4bbbb --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsLoadTestEnvironment.tests.ps1 @@ -0,0 +1,61 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-LoadTestEnvironment" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "" } + + Context "Testing Good Environment Names" { + + It "Returns true on Load Test Environment" { + Mock Get-AppSetting -ModuleName $moduleForMock { + param() + return "Catalyst vPod 4" + } + + Test-IsLoadTestEnvironment | Should -BeTrue + } + } + + Context "Testing Bad Environment Names" { + It "Returns false on partial vPod name pt.1" { + Mock Get-AppSetting -ModuleName $moduleForMock { + param() + return "CatalystvPod 4" + } + + Test-IsLoadTestEnvironment | Should -BeFalse + } + + It "Returns false on partial vPod name pt.2" { + Mock Get-AppSetting -ModuleName $moduleForMock { + param() + return "Catalyst vPod4" + } + + Test-IsLoadTestEnvironment | Should -BeFalse + } + + It "Returns false on Staging" { + Mock Get-AppSetting -ModuleName $moduleForMock { + param() + return "Staging Lane PS1" + } + + Test-IsLoadTestEnvironment | Should -BeFalse + } + + It "Returns false on Production" { + Mock Get-AppSetting -ModuleName $moduleForMock { + param() + return "Production Pod 12.4" + } + + Test-IsLoadTestEnvironment | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.ps1 new file mode 100644 index 0000000..60496bf --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.ps1 @@ -0,0 +1,31 @@ +filter Test-IsNull { + <# +.SYNOPSIS + Simple Null Coalesce Filter +.PARAMETER ValueA + Value to test for null. Returns this if it's not. +.PARAMETER ValueB + Value to return if ValueA is null +.PARAMETER Strict + Correctly handle the case where ValueA is $false. +.DESCRIPTION + Test if ValueA is null, and return ValueB if so. The legacy version mishandled the case where ValueA is $false. + The Strict flag was added as some existing functionality depends on that mishandling. +#> + param ( + $ValueA, + $ValueB, + [Switch]$Strict + ) + + $logLead = (Get-LogLeadName) + + if ($Strict) { + if (($null -ne $ValueA) -or !([string]::IsNullOrEmpty($valueA))) { $ValueA } else { $ValueB } + } else { + Write-Warning "$logLead : You are using the old version of this function. Unless you're counting on `$false being equivalent to `$null, you should add the `$Strict flag." + if ($null -ne $ValueA -and $ValueA -ne "") { $ValueA } else { $ValueB } + } +} + +Set-Alias IsNull Test-IsNull -Force -Scope:Global diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.tests.ps1 new file mode 100644 index 0000000..5a9335f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsNull.tests.ps1 @@ -0,0 +1,89 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Test-IsNull + +Describe "Legacy Test-IsNull" { + + Mock -ModuleName $moduleForMock Write-Warning -MockWith {} + Context "When Value A is Null" { + + It "Returns Value B" { + + Test-IsNull $null "ValueB" | Should -Be "ValueB" + } + } + + Context "When Value A is an Empty String" { + + It "Returns Value B" { + + Test-IsNull "" "ValueB" | Should -Be "ValueB" + } + } + + Context "When Value A is Not Null" { + + It "Returns Value A" { + + Test-IsNull "ValueA" "ValueB" | Should -Be "ValueA" + } + } + + Context "When Value A is `$false" { + + It "Returns ValueB" { + + Test-IsNull $false "ValueB" | Should -Be "ValueB" + } + } + + Context "When Called Without Strict Parameter" { + It "Warns The User They're Using An Old Version" { + Test-IsNull "ValueA" "ValueB" + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "You are using the old version of this function" } + } + } +} + +Describe "Strict Test-IsNull" { + + Context "When Value A is Null" { + + It "Returns Value B" { + + Test-IsNull $null "ValueB" -Strict | Should -Be "ValueB" + } + } + + Context "When Value A is an Empty String" { + + It "Returns an Empty String" { + + Test-IsNull "" "ValueB" -Strict | Should -Be "" + } + } + + Context "When Value A is Not Null" { + + It "Returns Value A" { + + Test-IsNull "ValueA" "ValueB" -Strict | Should -Be "ValueA" + } + } + + Context "When Value A is `$false" { + + It "Returns Value A `$false" { + + Test-IsNull $false "ValueB" -Strict | Should -Be $false + } + } +} + +#endregion Test-IsNull \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.ps1 new file mode 100644 index 0000000..56cfd06 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.ps1 @@ -0,0 +1,15 @@ +function Test-IsPsModuleInstalled { + <# +.SYNOPSIS + Wrapper around Get-Module -ListAvailable which returns the result in a useful boolean format. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("ModuleName")] + [string]$Name + ) + $result = Get-Module $name -ListAvailable + + return ($null -ne $result) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.tests.ps1 new file mode 100644 index 0000000..bf1c7fc --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsPsModuleInstalled.tests.ps1 @@ -0,0 +1,34 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Test-IsPsModuleInstalled + +Describe "Test-IsPsModuleInstalled" { + + Context "When Module Exists" { + It "Returns True" { + + Mock -ModuleName $moduleForMock Get-Module -MockWith { + return [PSCustomObject]@{ModuleType = "I am a fake module" } + } + + Test-IsPsModuleInstalled FakeModule | Should Be $true + } + } + + Context "When Module Does Not Exist" { + It "Returns False" { + + Mock -ModuleName $moduleForMock Get-Module { return $null } + + Test-IsPsModuleInstalled NonExistentModule | Should Be $false + } + } +} + +#endregion Test-IsPsModuleInstalled \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.ps1 new file mode 100644 index 0000000..0cfc046 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.ps1 @@ -0,0 +1,39 @@ +function Test-IsStringIPAddress { +<# +.SYNOPSIS + Determines if a string is an IP Address + +.DESCRIPTION + Compares a string against the regular expression "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + to determine if it is a valid IP address + +.PARAMETER stringToCheck + [string] A string to check if it is an IP + +.INPUTS + Accepts a string from the pipeline + +.OUTPUTS + A boolean indicating if the string is a valid IP address + +.EXAMPLE + Test-IsStringIPAddress "127.0.0.1" +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [string]$stringToCheck + ) + + $logLead = (Get-LogLeadName) + [Regex]$ipMatchRegex = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + + if ($stringToCheck -match $ipMatchRegex) { + Write-Verbose "$logLead : String $stringToCheck Matched IP Regex" + return $true + } + + Write-Verbose "$logLead : String $stringToCheck Does Not Match IP Address Regex" + return $false +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.tests.ps1 new file mode 100644 index 0000000..7b8ad80 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsStringIPAddress.tests.ps1 @@ -0,0 +1,77 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsStringIPAddress" { + + $validIPs = @( + + "127.0.0.1", + "192.168.1.1", + "10.50.1.150" + ) + + $invalidIPs = @( + + "256.0.0.1", + "999.999.999.999", + "1.1.1.257" + ) + + $testStrings = @( + + "HelloThereGeneralKenobi", + "IHateSand123", + "127001" + ) + + Context "When a Valid IP Address Is Specified" { + + foreach ($ip in $validIPs) { + + It "Returns True for $ip" { + + Test-IsStringIPAddress $ip | Should -BeTrue + } + } + } + + Context "When an Invalid IP-Like String is Specified" { + + foreach ($ip in $invalidIPS) { + + It "Returns False for $ip" { + + Test-IsStringIPAddress $ip | Should -BeFalse + } + } + } + + Context "When a String is Specified" { + + foreach ($testString in $testStrings) { + + It "Returns False for $testString" { + + Test-IsStringIPAddress $testString | Should -BeFalse + } + } + } + + Context "When a String is Piped To the Function" { + + It "Returns True if the String is an IP Address" { + + "127.0.0.1" | Test-IsStringIPAddress | Should -BeTrue + } + + It "Returns False if the String is Not an IP Address" { + + "IHaveTheHighGround" | Test-IsStringIPAddress | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlink.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlink.ps1 new file mode 100644 index 0000000..f24d4d4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlink.ps1 @@ -0,0 +1,43 @@ +function Test-IsSymlink { +<# +.SYNOPSIS + Test if a given file is a symlink + +.PARAMETER Path + [string] The path to test +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $loglead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($Path)) { + Write-Warning "$logLead : Path supplied was null or empty. Can not be a symlink." + return $false + } + + if (@('DirectoryInfo','FileInfo') -contains $Path.GetType().Name) { + $Path = $Path.FullName + } + + if (!(Test-Path $Path)) { + return $false + } + + $Path = Resolve-Path $Path + $item = (Get-Item $Path) + + # If the item is a Junction, SymbolicLink or a ReparsePoint, consider it a Symlink for + # the purposes of this function. (Junction and ReparsePoint are older and not found on modern Windows) + $returnItem = ( + ($item.Attributes.ToString() -match "ReparsePoint") ` + -or $item.IsJunction ` + -or $item.IsSymbolicLink ` + -or (@('SymbolicLink','Junction') -contains $item.LinkType)) + + return $returnItem +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlinkValid.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlinkValid.ps1 new file mode 100644 index 0000000..5b5b8e3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-IsSymlinkValid.ps1 @@ -0,0 +1,36 @@ +function Test-IsSymlinkValid { +<# +.SYNOPSIS + Used to test if an existing Symbolic Link path is linked to a filepath that is still on disk. + +.PARAMETER Path + Path to test +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $loglead = (Get-LogLeadName) + + if(Test-Path -Path $Path) { + $pathItem = Get-Item -Path $Path + } else { + Write-Warning "$loglead : Path is not found. Returning False." + return $false + } + + $isPathSymlink = ($pathItem.LinkType -eq "SymbolicLink") + + # Test if it's even a Symbolic Link. + if(!($isPathSymlink)) { + Write-Warning "$loglead : Path is not a SymbolicLink type. Returning False." + return $false + } + + # Check to see if the Target of the Symlink is real and on disk. + $isValid = (Test-Path -Path $pathItem.Target) + return $isValid +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.ps1 new file mode 100644 index 0000000..90c4301 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.ps1 @@ -0,0 +1,69 @@ +function Test-PathIsInApprovedPackageLocation { +<# +.SYNOPSIS + Test to ensure a path is in an approved package location. Could be in a chocolatey location or elsewhere. + +.PARAMETER SourcePath + [string] The path to the package install location + +.INPUTS + Requires the SourcePath + +.EXAMPLE + Test-PathIsInApprovedPackageLocation -SourcePath C:\ProgramData\chocolatey\lib\Alkami.Apps.Authentication + +This will return true + +.EXAMPLE + Test-PathIsInApprovedPackageLocation -SourcePath C:\OrbLogs\Alkami.Client.WebClient.log + +This obviously wrong test-case will return false +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$true, Position=0)] + [Alias('Path')] + [string]$SourcePath + ) + + $logLead = (Get-LogLeadName) + + $testPaths = @() + + ## Ensure the path actually exists + + if (!(Test-Path $SourcePath)) { + Write-Error "$logLead : Can not find path at [$SourcePath]" + return $false + } + + ## Ensure the path is actually a folder + if (!(Test-Path $SourcePath -PathType Container)) { + Write-Error "$logLead : Path at [$SourcePath] is not a folder" + return $false + } + + ## Add the chocolatey path for testing against + ## When we introduce a new package location, ex: we leave Chocolatey, we add those checks here too + $testPaths += (Get-ChocolateyInstallPath) + + $containsApprovedPath = $false + + foreach($testPath in $testPaths) { + ## Look for a test-path that matches the SourcePath exactly + ## This is an indeterminate case and is almost certainly wrong + $indeterminateTestPath = (Join-Path (Split-Path $TestPath) (Split-Path $TestPath -Leaf)) + $indeterminateSourcePath = (Join-Path (Split-Path $SourcePath) (Split-Path $SourcePath -Leaf)) + if ($indeterminateTestPath.ToLower() -eq $indeterminateSourcePath.ToLower()) { + $containsApprovedPath = $false + } + elseif ($SourcePath.ToLower().Contains($testPath.ToLower())) { + $containsApprovedPath = $true + } else { + $containsApprovedPath = $false + } + } + + return $containsApprovedPath +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.tests.ps1 new file mode 100644 index 0000000..eb9e567 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-PathIsInApprovedPackageLocation.tests.ps1 @@ -0,0 +1,32 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-PathIsInApprovedPackageLocation" { + + Context "Testing bad paths" { + + It "Return false when C:\Program Files is given" { + Test-PathIsInApprovedPackageLocation -SourcePath ("C:\Program Files\") | Should Be $false + } + } + + Context "Testing good paths" { + It "Returns true when a good path is given" { + Test-PathIsInApprovedPackageLocation -SourcePath (Join-Path (Get-ChocolateyInstallPath) "lib\Alkami.PowerShell.IIS") | Should Be $true + } + } + + Context "When an indeterminate path is given returns false" { + + It "Returns false for Get-ChocolateyInstallPath" { + Test-PathIsInApprovedPackageLocation -SourcePath (Get-ChocolateyInstallPath) | Should Be $false + } + } +} + +#endregion Test-IsNull \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-PathMatch.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-PathMatch.ps1 new file mode 100644 index 0000000..67ea7b2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-PathMatch.ps1 @@ -0,0 +1,60 @@ +function Test-PathMatch { +<# +.SYNOPSIS + Used to test if two file paths are equal. This is useful to determine if two paths are the same even if the + Symlink name is different. + +.PARAMETER FirstPath + First path to test + +.PARAMETER SecondPath + Second path to test + +.NOTES + Files that are the target of Symlinks can have multiple Symlinks targeting a given file or folder. + This function will handle many targets of a linked file/folder. + +.EXAMPLE + New-Symlink Source 'C:\ProgramData\chocolatey\lib\Alkami.Apps.Authentication\content\Areas\App' -Target 'C:\orb\WebClient\Areas\' -Name 'Test' + Test-PathMatch -Source 'C:\ProgramData\chocolatey\lib\Alkami.Apps.Authentication\content\Areas\App' -Target 'C:\orb\WebClient\Areas\Test' + +.OUTPUTS + [bool] $True if the two paths match, $False if not +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $true)] + [Alias("Source")] + [string]$FirstPath, + + [Parameter(Mandatory = $true)] + [Alias("Target")] + [string]$SecondPath + ) + + if ([string]::IsNullOrWhiteSpace($FirstPath) -or [string]::IsNullOrWhiteSpace($SecondPath)) { + # Can't really test if an input is null + return $false + } + + $path1 = Get-Item $FirstPath + $path2 = Get-Item $SecondPath + $path1FullPath = [array]$path1.FullName.Trim('\') + $path2FullPath = [array]$path2.FullName.Trim('\') + + $isFirstPathSymlink = ($path1.LinkType -eq "SymbolicLink") + $isSecondPathSymlink = ($path2.LinkType -eq "SymbolicLink") + + if($isFirstPathSymlink) { + $path1FullPath = $path1.Target.Trim('\') + } + + if($isSecondPathSymlink) { + $path2FullPath = $path2.Target.Trim('\') + } + + # If any of the items in the array match, return $true + $isMatch = ($path1FullPath | Where-Object{$_ -ieq $path2FullPath}).Count -gt 0 + return $isMatch +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.ps1 new file mode 100644 index 0000000..d6b96bb --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.ps1 @@ -0,0 +1,32 @@ +function Test-PathsAreEqual { +<# +.SYNOPSIS + Verify two paths are the same + +.PARAMETER Path + The source path to compare against + +.PARAMETER Target + The target path to compare to +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [Alias('From','Source')] + $Path, + [Parameter(Mandatory = $true, Position = 1)] + [ValidateNotNullOrEmpty()] + [Alias('To')] + $Target + ) + + # Do not do a test-path because they may not exist. + # We may be creating a file or something + + $Target = (Join-Path $Target '') + $Path = (Join-Path $Path '') + + return $Target -eq $Path +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.tests.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.tests.ps1 new file mode 100644 index 0000000..55a4597 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-PathsAreEqual.tests.ps1 @@ -0,0 +1,43 @@ +# . $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-PathsAreEqual" { + Context "Two paths are equivalent" { + It "Returns true" { + Test-PathsAreEqual "C:\" "C:\" | Should -BeTrue + } + + It "Returns true even if one is missing a trailing slash" { + Test-PathsAreEqual "C:\Fake" "C:\Fake\" | Should -BeTrue + } + } + + Context "Two paths are not equivalent" { + It "Returns false" { + Test-PathsAreEqual "C:\Fake Path" "C:\Fake" | Should -BeFalse + } + + It "Returns falsee even if one is missing a trailing slash" { + Test-PathsAreEqual "C:\Fake Path" "C:\Fake\" | Should -BeFalse + } + } + + Context "Throws when either param are null" { + It "First param is `$null" { + { Test-PathsAreEqual $null "C:\" } | Should -Throw + } + + It "Second param is `$null" { + { Test-PathsAreEqual "C:\" $null } | Should -Throw + } + + It "Both params are `$null" { + { Test-PathsAreEqual $null $null } | Should -Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-ShouldContinue.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-ShouldContinue.ps1 new file mode 100644 index 0000000..4caeea8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-ShouldContinue.ps1 @@ -0,0 +1,81 @@ +function Test-ShouldContinue { + + <# + .SYNOPSIS + A wrapper for PSCmdlet.ShouldContinue to enable Pester testing. + + .DESCRIPTION + A wrapper for PSCmdlet.ShouldContinue to enable Pester testing. + + .PARAMETER Query + Textual query of whether the action should be performed, usually in the form of a question. + + .PARAMETER Caption + Caption of the window which may be displayed when the user is prompted whether or not to perform the action. It may be displayed by some hosts, but not all. + + .PARAMETER HasSecurityImpact + true if the operation being confirmed has a security impact. If specified, the default option selected in the selection menu is 'No'. + + .PARAMETER YesToAll + true if user selects YesToAll. If this is already true, ShouldContinue will bypass the prompt and return true. + + .PARAMETER NoToAll + true if user selects NoToAll. If this is already true, ShouldContinue will bypass the prompt and return false. + + .PARAMETER Force + When specified, always returns true. Added based on recommendations in the official documentation to support this option + + .LINK + https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.cmdlet.shouldcontinue?view=powershellsdk-1.1.0 + #> + [CmdletBinding(DefaultParameterSetName = 'SimpleSet')] + [OutputType([System.Boolean])] + param ( + + [Parameter(ParameterSetName = 'SimpleSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ToAllSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'SecurityImpactSet', Mandatory = $true)] + [string]$Query, + + [Parameter(ParameterSetName = 'SimpleSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ToAllSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'SecurityImpactSet', Mandatory = $true)] + [string]$Caption, + + [Parameter(ParameterSetName = 'SecurityImpactSet', Mandatory = $true)] + [bool]$HasSecurityImpact, + + [Parameter(ParameterSetName = 'ToAllSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'SecurityImpactSet', Mandatory = $true)] + [ref]$YesToAll, + + [Parameter(ParameterSetName = 'ToAllSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'SecurityImpactSet', Mandatory = $true)] + [ref]$NoToAll, + + [Parameter(ParameterSetName = '__AllParameterSets', Mandatory = $false)] + [switch]$Force + ) + + if ($Force.IsPresent) { + + return $true + } + + $parameterSet = $PSCmdlet.ParameterSetName + + if ($parameterSet -eq "SimpleSet") { + + return $PSCmdlet.ShouldContinue($Query, $Caption) + } + + if ($parameterSet -eq "ToAllSet") { + + return $PSCmdlet.ShouldContinue($Query, $Caption, $YesToAll, $NoToAll) + } + + if ($parameterSet -eq "SecurityImpactSet") { + + return $PSCmdlet.ShouldContinue($Query, $Caption, $HasSecurityImpact, $YesToAll, $NoToAll) + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-ShouldProcess.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-ShouldProcess.ps1 new file mode 100644 index 0000000..a297256 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-ShouldProcess.ps1 @@ -0,0 +1,91 @@ +function Test-ShouldProcess { + + <# + .SYNOPSIS + A wrapper for PSCmdlet.ShouldProcess to enable Pester testing. + + .DESCRIPTION + A wrapper for PSCmdlet.ShouldProcess to enable Pester testing. + + .PARAMETER VerboseDescription + Textual description of the action to be performed. This is what will be displayed to the user for ActionPreference.Continue. + + .PARAMETER VerboseWarning + Textual query of whether the action should be performed, usually in the form of a question. This is what will be displayed to the user for ActionPreference.Inquire. + + .PARAMETER Caption + Caption of the window which may be displayed if the user is prompted whether or not to perform the action. caption may be displayed by some hosts, but not all. + + .PARAMETER ShouldProcessReason + Indicates the reason(s) why ShouldProcess returned what it returned. Only the reasons enumerated in ShouldProcessReason are returned. + + .PARAMETER Target + Name of the target resource being acted upon. This will potentially be displayed to the user. + + .PARAMETER Action + Name of the action which is being performed. This will potentially be displayed to the user. (default is Cmdlet name) + + .PARAMETER Force + When specified, always returns true. Added based on recommendations in the official documentation to support this option + + .LINK + https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.cmdlet.shouldprocess?view=powershellsdk-1.1.0 + #> + + [CmdletBinding(DefaultParameterSetName = 'TargetActionSet', SupportsShouldProcess = $true)] + [OutputType([System.Boolean])] + param ( + + [Parameter(ParameterSetName = 'VerboseSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ProcessReasonSet', Mandatory = $true)] + [string]$VerboseDescription, + + [Parameter(ParameterSetName = 'VerboseSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ProcessReasonSet', Mandatory = $true)] + [string]$VerboseWarning, + + [Parameter(ParameterSetName = 'VerboseSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'ProcessReasonSet', Mandatory = $true)] + [string]$Caption, + + [Parameter(ParameterSetName = 'ProcessReasonSet', Mandatory = $true)] + [string]$ShouldProcessReason, + + [Parameter(ParameterSetName = 'TargetActionSet', Mandatory = $true)] + [Parameter(ParameterSetName = 'TargetSet', Mandatory = $true)] + [string]$Target, + + [Parameter(ParameterSetName = 'TargetActionSet', Mandatory = $true)] + [string]$Action, + + [Parameter(ParameterSetName = '__AllParameterSets', Mandatory = $false)] + [switch]$Force + ) + + if ($Force.IsPresent) { + + return $true + } + + $parameterSet = $PSCmdlet.ParameterSetName + + if ($parameterSet -eq "VerboseSet") { + + return $PSCmdlet.ShouldProcess($VerboseDescription, $VerboseWarning, $Caption) + } + + if ($parameterSet -eq "ProcessReasonSet") { + + return $PSCmdlet.ShouldProcess($VerboseDescription, $VerboseWarning, $Caption, [ref]$ShouldProcessReason) + } + + if ($parameterSet -eq "TargetActionSet") { + + return $PSCmdlet.ShouldProcess($Target, $Action) + } + + if ($parameterSet -eq "TargetSet") { + + return $PSCmdlet.ShouldProcess($Target) + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrEmpty.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrEmpty.ps1 new file mode 100644 index 0000000..01404c2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrEmpty.ps1 @@ -0,0 +1,26 @@ +function Test-StringIsNullOrEmpty { + <# + .SYNOPSIS + A wrapper for IsNullorEmpty to assist with Pester tests. + + .DESCRIPTION + Ingests a value to test if it is Null or Empty. + + .PARAMETER Value + Value to insert into the IsNullOrEmpty command. + + .EXAMPLE + Test-StringIsNullOrEmpty -Value "Test" + + False + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $false)] + [string]$Value + ) + + # This function is just a convenience wrapper to allow us to mock them in Pester tests + return ([string]::IsNullOrEmpty($Value)) + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrWhitespace.ps1 b/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrWhitespace.ps1 new file mode 100644 index 0000000..dbdcd39 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Test-StringIsNullOrWhitespace.ps1 @@ -0,0 +1,26 @@ +function Test-StringIsNullOrWhitespace { + <# + .SYNOPSIS + A wrapper for IsNullOrWhitespace to assist with Pester tests. + + .DESCRIPTION + Ingests a value to test if it is Null or Empty. + + .PARAMETER Value + Value to insert into the IsNullOrWhitespace command. + + .EXAMPLE + Test-StringIsNullOrWhitespace -Value "Test" + + False + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $false)] + [string]$Value + ) + + # This function is just a convenience wrapper to allow us to mock them in Pester tests + return ([string]::IsNullOrWhitespace($Value)) + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Use-Module.ps1 b/Modules/Alkami.PowerShell.Common/Public/Use-Module.ps1 new file mode 100644 index 0000000..fd98cdf --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Use-Module.ps1 @@ -0,0 +1,35 @@ +function Use-Module { +<# +.SYNOPSIS + Load a module only once. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [Alias("ModuleName")] + [string]$Name + ) + process { + $logLead = (Get-LogLeadName); + + if (!$Name) { throw "Loading of a module requires a name"; } + + Write-Verbose "$logLead : Attempting to load module $Name"; + + try { + if (!(Get-Module $Name)) { + Import-Module $Name -Global -NoClobber -WarningAction SilentlyContinue; + } + } catch { + Write-Verbose "Could not load the module with the safe option of Get-Module so force loading it"; + try { + Import-Module $Name -Global -NoClobber -WarningAction SilentlyContinue; + } catch { + Write-Verbose "Could not install the module $name -- does it exist on this environment?"; + } + } + + Write-Verbose "$logLead : Done" + } +} + diff --git a/Modules/Alkami.PowerShell.Common/Public/Wait-ServersAreReachable.ps1 b/Modules/Alkami.PowerShell.Common/Public/Wait-ServersAreReachable.ps1 new file mode 100644 index 0000000..4f6ec7f --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Wait-ServersAreReachable.ps1 @@ -0,0 +1,99 @@ +function Wait-ServersAreReachable { + <# + .Synopsis + Waits on the list of $Servers to become reachable by WinRM with a timeout. + .Parameter Servers + The servers to connect to and wait for. + .Parameter TimeoutMinutes + The number of minutes to wait until this function times out. + .Parameter RetryDelaySeconds + The number of seconds to wait between WinRM connection test attempts. + #> + Param ( + [Parameter(Mandatory = $true)] + [string[]]$Servers, + [Parameter(Mandatory = $false)] + [int]$TimeoutMinutes = 10, + [Parameter(Mandatory = $false)] + [int]$RetryDelaySeconds = 5 + ) + + $logLead = (Get-LogLeadName) + + # if there are no servers to wake up, skip it! + if(Test-IsCollectionNullOrEmpty $Servers) { + Write-Host "$logLead : No servers to act on. Skipping.." + return + } + + $csvServers = $Servers -join ", " + Write-Host "$logLead : Waiting on Servers to Become Reachable: $csvServers" + + # Create counters to track how many servers have been reached, and start the timer. + $connectCount = 0 + $totalConnected = 0 + $serverCount = $Servers.Count + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + # Assign Servers to a ServerList, which we will whittle down as servers become reachable. + $serversNotConnectedTo = $Servers + Write-Host "$logLead : Attempting to connect to all servers. Will retry to connect for $TimeoutMinutes minutes for any that can't connect." + + while($connectCount -lt $serverCount) { + $hostResults = Invoke-Parallel -objects $serversNotConnectedTo -returnObjects -numThreads 32 -script { + param($server) + $session = $null + $logLead = "[Wait-ServersAreReachable]" + try { + $so = New-PSSessionOption -OpenTimeout 10000 -CancelTimeout 30000 -OperationTimeout 30000 -MaxConnectionRetryCount 1 + $session = New-PSSession $server -ErrorAction SilentlyContinue -SessionOption $so + $success = (1 -eq (Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock { return 1 })) + + if($success) { + Write-Host "$logLead : Connected successfully to $server" + return @{"y" = $server} # Single Hashtable added as an item in array $hostResults + } else { + + # Write-Verbose instead of Write-Host to reduce chattiness. The servers that are failing are written out in the end. + Write-Verbose "$logLead : Could not connect to $server" + return @{"n" = $server} # Single Hashtable added as an item in array $hostResults + } + } catch { + Write-Verbose "$logLead : Error connecting to $server" + return @{"n" = $server} # Single Hashtable added as an item in array $hostResults + } finally { + if($null -ne $session) { + Remove-PSSession $session + } + } + } + + # Get count of servers we can connect to. Array of Hashtables, filtered on the Keys, value = server name + $connectCount = ( ($hostResults | Where-Object{$_.Keys -eq "y"}).Values ).Count + $totalConnected += $connectCount + + # Replace $serversNotConnectedTo with the remaining servers that are failing, so that we do not re-test good servers. + $serversNotConnectedTo = ($hostResults | Where-Object{$_.Keys -eq "n"}).Values + + Write-Verbose "$logLead : connectCount = $connectCount" + Write-Verbose "$logLead : totalConnected = $totalConnected" + Write-Verbose "$logLead : serverCount = $serverCount" + + if(([int]$stopWatch.Elapsed.TotalMinutes) -ge $TimeoutMinutes) { + Write-Warning "$logLead : Exceeded timeout of $TimeoutMinutes minutes. Exiting." + break + } + + if($totalConnected -lt $serverCount) { + Write-Host "$logLead : Connected to $totalConnected out of $serverCount servers. Sleeping $RetryDelaySeconds seconds." + Start-Sleep -Seconds $RetryDelaySeconds + } else { + Write-Host "$logLead : Successfully connected to all servers." + break + } + } + + if($totalConnected -lt $serverCount) { + throw "$logLead : Couldn't connect to all servers in alloted time of $TimeoutMinutes minutes.`nCouldn't connect to: $serversNotConnectedTo " + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/Public/Write-ArrayToOutput.ps1 b/Modules/Alkami.PowerShell.Common/Public/Write-ArrayToOutput.ps1 new file mode 100644 index 0000000..4b57416 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/Public/Write-ArrayToOutput.ps1 @@ -0,0 +1,17 @@ +function Write-ArrayToOutput { +<# +.SYNOPSIS + Iterates an array of items and writes them out. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [object]$items + ) + + $items | ForEach-Object { + Write-Output $_ + } +} + diff --git a/Modules/Alkami.PowerShell.Common/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.Common/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..13f46ed --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/tools/chocolateyInstall.ps1 @@ -0,0 +1,36 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Common/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.Common/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.Common/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.nuspec b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.nuspec new file mode 100644 index 0000000..16d41a3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.nuspec @@ -0,0 +1,31 @@ + + + + Alkami.PowerShell.Configuration + $version$ + Alkami Platform Modules - PowerShell - Configuration + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.Configuration + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Configuration module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.psd1 b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.psd1 new file mode 100644 index 0000000..1b1c537 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.psd1 @@ -0,0 +1,9 @@ +@{ + RootModule = 'Alkami.PowerShell.Configuration.psm1' + ModuleVersion = '3.26.1' + Description = 'A set of functions for managing server configurations.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.AD' + FunctionsToExport = 'Add-EagleEyeConfig','Add-NetshExcludedPortRange','Copy-AlkamiLog4net','Find-CertificateBySubjectOrSAN','Get-AllAppSettingKeys','Get-AllCertificatesFromSpecificStore','Get-AppServiceAccountName','Get-AppSetting','Get-ConfigSetting','Get-ConfigurationFiles','Get-EnvironmentDesignation','Get-EnvironmentHosting','Get-EnvironmentName','Get-EnvironmentNameSafeDesignation','Get-EnvironmentServer','Get-EnvironmentType','Get-EnvironmentUserPrefix','Get-HelmApplicationYamls','Get-HelmDeploymentMetadata','Get-LogPathsForOrbApplication','Get-MachineConfigServiceAccount','Get-NetshExcludedPortRanges','Get-NetshHttpIPListens','Get-NewRelicAppNameForConfigurationValue','Get-NewRelicNextLink','Get-NewRelicObjects','Get-OrbSymLinkFolderNames','Get-PackageManifest','Get-RedisConnectionString','Get-RegistryKeyValue','Get-RemoteDotNetConfigPath','Get-ReportServerConfiguration','Get-ServerRoleEnvironmentalVariable','Get-ValidPackageDatabaseConfigFilenames','Get-ValidPackageManifestFilenames','Get-ValidWebTierInstallLocations','New-AlkamiManifest','New-MachineConfigConnectionString','New-MachineConfigMachineKeys','New-OrbSymLinks','New-RegistryKey','New-VIPsHostFileEntries','New-WebTierHostFileEntries','Remove-AppSetting','Remove-EnvironmentVariable','Remove-NetshExcludedPortRange','Rename-NewLogConfig','Rename-TemporaryConfigFiles','Set-AlkamiConfigs','Set-AppSetting','Set-BeaconFeatureSettings','Set-DefaultNetshIPListens','Set-DefaultNetshURLACLS','Set-DefaultTLSVersion','Set-EagleEyePermissions','Set-EnvironmentDesignation','Set-EnvironmentHosting','Set-EnvironmentName','Set-EnvironmentNameSafeDesignation','Set-EnvironmentServer','Set-EnvironmentType','Set-EnvironmentUserPrefix','Set-EnvironmentVariable','Set-HelmDeploymentVersions','Set-NewRelicAppName','Set-NewRelicAppNameConfigFileValue','Set-ORBEnvironmentalVariables','Set-RedisConnectionString','Set-RegistryKeyValue','Set-ServerRoleEnvironmentalVariable','Set-SMSvcHostSids','Set-StaticConfigValues','Test-AlkamiApiComponentManifest10','Test-AlkamiFluentMigrationManifest10','Test-AlkamiHotfixManifest10','Test-AlkamiInstallerManifest10','Test-AlkamiLegacyUtilityManifest10','Test-AlkamiManifest','Test-AlkamiManifestGeneral10','Test-AlkamiProviderManifest10','Test-AlkamiReportManifest10','Test-AlkamiRepositoryManifest10','Test-AlkamiServiceManifest10','Test-AlkamiSREModuleManifest10','Test-AlkamiWebApplicationManifest10','Test-AlkamiWebExtensionManifest10','Test-AlkamiWebsiteManifest10','Test-AlkamiWidgetManifest10','Test-IsAppServer','Test-IsAws','Test-IsBuildEnvironment','Test-IsDeveloperMachine','Test-IsEntrustServer','Test-IsEnvironment','Test-IsMicServer','Test-IsOverflowServer','Test-IsProductionEnvironment','Test-IsQaEnvironment','Test-IsSecureEnvironment','Test-IsServiceFabricServer','Test-IsUnconfiguredEnvironment','Test-IsWebServer','Test-IsWindowsServer','Test-IsWindowsServerCore','Test-ORBEnvironmentalVariables','Test-OrbSymLinksExist','Test-RegistryKey','Update-AWSPowershellModule','Update-SystemPortReservations' + AliasesToExport = 'Create-MachineConfigConnectionString','Create-MachineConfigMachineKeys','Create-VIPsHostFileEntries','Create-WebTierHostFileEntries','IsAppServer','IsAws','IsMicroServer','IsWebServer','Test-IsDevelopmentEnvironment','Test-IsMicroServer' +} diff --git a/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.pssproj b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.pssproj new file mode 100644 index 0000000..80cf005 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Alkami.PowerShell.Configuration.pssproj @@ -0,0 +1,98 @@ + + + Debug + 2.0 + {1b9dc54a-50dc-4b25-8744-92c7eca40092} + Exe + MyApplication + MyApplication + Alkami.PowerShell.Configuration + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.Configuration") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Configuration/AlkamiManifest.xml new file mode 100644 index 0000000..3b51ac5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.Configuration + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateJson.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateJson.ps1 new file mode 100644 index 0000000..f45acd8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateJson.ps1 @@ -0,0 +1,67 @@ +function Get-AppSettingPrivateJson { +<# +.SYNOPSIS + Returns an appSetting value from the specified file content + +.DESCRIPTION + Returns an appSetting value from the specified file content + +.PARAMETER Key + [string] The name of the key to get the value from, if it exists. + +.PARAMETER FileContent + [string] The file content to parse + +.PARAMETER SuppressWarnings + [switch] Suppress warnings about keys not found + +.OUTPUTS + Can return $null if key not found +#> + [CmdletBinding()] + param ( + [string]$Key, + [string]$FileContent, + [switch]$SuppressWarnings + ) + + $logLead = (Get-LogLeadName) + + $json = (ConvertFrom-Json -InputObject $FileContent) + + # appsettings.json has a magic trick for us + # if I specify Parent:Child then I'm looking for something like + # { "Parent": { "Child": value } } + # if I specify Parent.Child then I'm looking for something like + # { "Parent.Child": value } + # So we want to be able to do a depth search by the colon'd parts + $keyNodeList = $Key.Split(':') + if (Test-IsCollectionNullOrEmpty $keyNodeList) { + throw "$logLead : For config file [$FilePath] - No valid key specified - Can not continue. Key was [$Key]" + } + foreach($node in $keyNodeList) { + if ([string]::IsNullOrWhiteSpace($node)) { + throw "$logLead : For config file [$FilePath] - Found invalid node element [$node] in Key path [$Key]" + } + } + + $lastNode = "" + $tempNode = $json + foreach($node in $keyNodeList) { + Write-Verbose "$logLead : Checking `$tempNode [$tempNode] for property `$node [$node]" + + if (($node -eq $keyNodeList[-1]) -or ($null -eq $tempNode.$node)) { + if ($null -eq $tempNode.$node) { + if ($SuppressWarnings) { + Write-Verbose "$logLead : Can not find [$node] of [$Key] for element under [$lastNode]. Returning `$null" + } else { + Write-Warning "$logLead : Can not find [$node] of [$Key] for element under [$lastNode]. Returning `$null" + } + } + return $tempNode.$node + } + + $tempNode = $tempNode.$node + $lastNode = $node + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateXml.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateXml.ps1 new file mode 100644 index 0000000..9fd59f5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Get-AppSettingPrivateXml.ps1 @@ -0,0 +1,107 @@ +function Get-AppSettingPrivateXml { +<# +.SYNOPSIS + Returns an appSetting value from the specified file content + +.DESCRIPTION + Returns an appSetting value from the specified file content + +.PARAMETER Key + [string] The name of the key to get the value from, if it exists. + +.PARAMETER FileContent + [xml] The file content to parse + +.PARAMETER SuppressWarnings + [switch] Suppress warnings about keys not found + +.OUTPUTS + Can return $null if key not found +#> + [CmdletBinding()] + param ( + [string]$Key, + [xml]$FileContent, + [switch]$SuppressWarnings + ) + + $logLead = (Get-LogLeadName) + + ## Beware of the case of more than one appSettings element in case we run into issues, le sigh + ## Let's go ahead and let the person reading the logs know that there were more than one section + + $appSettingsCollection = @($FileContent.SelectNodes('//appSettings')) + + if ($appSettingsCollection.Count -gt 1) { + Write-Warning "$logLead : More than one appSettings section found to manipulate in [$FilePath]" + foreach($appSettingElement in $appSettingsCollection) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + Write-Host "Too many appSettings: $fullPath" + } + } + + if ($appSettingsCollection.Count -eq 0) { + ## We don't have an appSettings element to work with + ## Announce that, then return $null + + Write-Warning "$logLead : Could not find an appSettings element at all in [$FilePath]. Returning `$null" + return $null + } + + ## Now that we have at least one location for settings, let's go see if we have an existing value + + $existingSettingValuesByKey = @($FileContent.configuration.SelectNodes("//appSettings/add[@key='$Key']")) + + ## Crap, we found more than one element with the same key. + ## We can't resolve this, but honestly the system can't work with it this way either + ## Something will absolutely break, make sure that's logged + ## At the very least, let's give something for the app to work with, I guess + if ($existingSettingValuesByKey.length -gt 1) { + Write-Error "Found too many nodes with the same Key value in [$FilePath]!!" + + foreach($appSettingElement in $existingSettingValuesByKey) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + Write-Host "Too many nodes: $fullPath" + } + + ## Return the last value found as it is the most general setting. + ## Honestly we should have a hard-break here because this should be resolved + Write-Warning "$logLead : Returning the last found value. Fingers crossed." + return $existingSettingValuesByKey[-1].Value + } + + ## Now we either have a value in $existingSettingValuesByKey or we don't + ## If we have one, we return that value, else return $null + + if ($existingSettingValuesByKey.length -eq 0) { + ## We don't have a value + if ($SuppressWarnings) { + Write-Verbose "$logLead : Could not find appSetting key '$Key'." + } else { + Write-Warning "$logLead : Could not find appSetting key '$Key'." + } + + return $null + } else { + ## We have the key and there's only one in the collection! + return @($existingSettingValuesByKey)[0].Value + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Remove-AppSettingPrivate.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Remove-AppSettingPrivate.ps1 new file mode 100644 index 0000000..0cd132b --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Remove-AppSettingPrivate.ps1 @@ -0,0 +1,72 @@ +function Remove-AppSettingPrivate { +<# +.SYNOPSIS + Removes an appSetting Key/Value pair in the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Remove an appSetting key/value pair in the specified config file if present. + Will default to the global 64 bit machine.config file if no config file value specified. + Will not tickle files where no values have changed. + Can connect to remote computers as well. + +.PARAMETER Key + [string] The appSetting key + +.PARAMETER FilePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + param ( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + $logLead = (Get-LogLeadName) + $dirty = $false + + $appSettingsCollection = @($xml.SelectNodes('//appSettings')) + + if (Test-IsCollectionNullOrEmpty $appSettingsCollection) { + Write-Warning "$logLead : No appSettings section found in file. Can not modify." + return + } + + # In case there are more than one appSettings sections in the file + # There are edge cases where this may occur (notably some web.config) + # In the worst/best? case this just affects the one location + foreach($appSettingElement in $appSettingsCollection) { + $addNodes = $appSettingElement.SelectNodes('add') + foreach($node in $addNodes) { + if($node.key -eq $Key) { + Write-Host "$logLead : Found matching [$Key], removing" + $appSettingElement.RemoveChild($node) | Out-Null # in case it contains a password or something + $dirty = $true + } + } + } + + if($dirty) { + Write-Verbose "$logLead : Saving config to path [$FilePath]" + + if ($Force -or $PSCmdlet.ShouldProcess("Ready to write the file [$FilePath] contents?")) { + ## Make sure the file gets saved with NoBOM + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + + Save-XMLFile $FilePath $xml.OuterXml $utfNoBOM + } else { + Write-Host "$logLead : When confirmed, will write to file [$FilePath] with contents [[$($xml.OuterXml)]]" + } + } else { + Write-Verbose "$logLead : No changes were made to $FilePath" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivate.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivate.ps1 new file mode 100644 index 0000000..5def1a7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivate.ps1 @@ -0,0 +1,162 @@ +function Set-AppSettingPrivate { +<# +.SYNOPSIS + Sets an appSetting Key/Value pair in the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Set an appSetting key/value pair in the specified config file. + Will default to the global 64 bit machine.config file if no config file value specified. + Will not tickle files where no values have changed. + Can connect to remote computers as well. + +.PARAMETER key + [string] The appSetting key + +.PARAMETER value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER filePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. + +.PARAMETER UpdateOnly + [switch] Only updates a key's value if it exists. Will not create the missing AppSetting key. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + param ( + [Parameter(Mandatory = $true)] + [string]$key, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$value, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$UpdateOnly + ) + + $logLead = (Get-LogLeadName) + $dirty = $false + + ## Beware of the case of more than one appSettings element in case we run into issues, le sigh + ## Let's go ahead and let the person reading the logs know that there were more than one section + ## In the case of multiple found, we want to save the shortest-path to add our settings to in the future + ## We should track that as we calculate our paths + + $shortestAppSettingsPathSize = [int]::MaxValue # this keeps us having to do 0 checks as well + $appSettingsCollection = @($xml.SelectNodes('//appSettings')) + $preferredAppSettingsLocation = $appSettingsCollection[0] + + if ($appSettingsCollection.Count -gt 1) { + Write-Warning "$logLead : More than one appSettings section found to manipulate in [$FilePath]" + foreach($appSettingElement in $appSettingsCollection) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + if ($shortestAppSettingsPathSize -lt $fullPath.length) { + $preferredAppSettingsLocation = $appSettingElement + $shortestAppSettingsPathSize = $fullPath.length + } + Write-Host $fullPath + } + } + + if (Test-IsCollectionNullOrEmpty $appSettingsCollection) { + ## We don't have an appSettings element to work with + ## Let's add one and then store it for later use + Write-Warning "$logLead : No appSettings section found to manipulate in [$FilePath], creating one on element" + [void]$xml.SelectSingleNode("configuration").AppendChild($xml.CreateElement("appSettings")) + $preferredAppSettingsLocation = @($xml.SelectNodes('//appSettings'))[0] + $dirty = $true + } + + ## Now that we have a location to store any settings, let's go see if we have an existing value to update first + $existingSettingValuesByKey = @($xml.configuration.SelectNodes("//appSettings/add[@key='$key']")) + + ## Crap, we found more than one element with the same key. + ## We can't edit this, but honestly the system can't work with it this way either + ## Something will absolutely break, but it doesn't have to be us today + if ($existingSettingValuesByKey.length -gt 1) { + Write-Error "$logLead : Found too many nodes with the same Key value in [$FilePath]!!" + + foreach($appSettingElement in $existingSettingValuesByKey) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + Write-Host "$logLead : Duplicate node found at: $fullPath" + $appSettingElement.Value = $value + $dirty = $true + } + } + + # If the UpdateOnly switch is specified, and an AppSetting was not found, early out of saving anything. + if ($existingSettingValuesByKey.length -eq 0 -and $UpdateOnly) { + Write-Host "$logLead : Could not find appSetting key '$key'. Not creating AppSetting node because UpdateOnly was specified. Exiting.." + return + } + + ## Now we either have a value in $existingSettingValuesByKey or we don't + ## If we have one, we set that value to the requested value + ## If we don't have an element there, we create one and stuff it on $preferredAppSettingsLocation + ## We should track our dirty status so we only update the file if it has to be touched + ## This last part is important for web.config which will bounce a webapp + if ($existingSettingValuesByKey.length -eq 0) { + ## We don't have a value yet in the file, let's add it to that $preferredAppSettingsLocation we found before + Write-Host "$logLead : Could not find appSetting key '$key'. Creating one and setting value to '$value'." + + $dirty = $true + + $appSettingElement = $xml.CreateElement("add") + $appSettingElement.SetAttribute("key", $key) + $appSettingElement.SetAttribute("value", $value) + [void]$preferredAppSettingsLocation.AppendChild($appSettingElement) + } else { + ## We have the key already, let's update the value + $existingValue = @($existingSettingValuesByKey)[0].Value + + ## Ensure that if the case is changed that we for sure update the value (CNE compare) + if ($existingValue -cne $value) { + Write-Host "$logLead : Found appSetting '$key', changing value from '$existingValue' to '$value'." + $dirty = $true + } + @($existingSettingValuesByKey)[0].Value = $value + } + + if($dirty) { + Write-Verbose "$logLead : Saving config to path $FilePath" + + if ($Force -or $PSCmdlet.ShouldProcess("Ready to write the file [$FilePath] contents?")) { + ## Make sure the file gets saved with NoBOM + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + + Save-XMLFile $FilePath $xml.OuterXml $utfNoBOM + } else { + Write-Host "$logLead : When confirmed, will write to file [$FilePath] with contents [[$($xml.OuterXml)]]" + } + } else { + Write-Verbose "$logLead : No changes were made to $FilePath" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateJson.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateJson.ps1 new file mode 100644 index 0000000..6b0a43c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateJson.ps1 @@ -0,0 +1,128 @@ +function Set-AppSettingPrivateJson { +<# +.SYNOPSIS + Sets an appSetting Key/Value pair in the specified file. + This does so according to the JSON appSettings.json expected schema. + +.DESCRIPTION + Sets an appSetting Key/Value pair in the specified file. + This does so according to the JSON appSettings.json expected schema. + Can connect to remote computers as well. + +.PARAMETER Key + [string] The appSetting key + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER filePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. + +.PARAMETER UpdateOnly + [switch] Only updates a key's value if it exists. Will not create the missing AppSetting key. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + param ( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Value, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath, + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$UpdateOnly + ) + + $logLead = (Get-LogLeadName) + $dirty = $false + + $json = (ConvertFrom-Json -InputObject (Get-Content -Path $FilePath -Raw)) + + # appsettings.json has a magic trick for us + # if I specify Parent:Child then I'm looking for something like + # { "Parent": { "Child": value } } + # if I specify Parent.Child then I'm looking for something like + # { "Parent.Child": value } + # So we want to be able to do a depth search by the colon'd parts + $keyNodeList = $Key.Split(':') + if (Test-IsCollectionNullOrEmpty $keyNodeList) { + throw "$logLead : For config file [$FilePath] - No valid key specified - Can not continue. Key was [$Key]" + } + foreach($node in $keyNodeList) { + if ([string]::IsNullOrWhiteSpace($node)) { + throw "$logLead : For config file [$FilePath] - Found invalid node element [$node] in Key path [$Key]" + } + } + + $lastNode = "" + $tempNode = $json + foreach($node in $keyNodeList) { + Write-Verbose "$logLead : Checking `$tempNode [$tempNode] for property `$node [$node]" + + # If the UpdateOnly switch is specified, and an AppSetting was not found, early out of saving anything. + if ($UpdateOnly -and ($null -eq $tempNode.$node)) { + Write-Warning "$logLead : For config file [$FilePath] - Could not find specified Key [$Key]. Not creating AppSetting node because UpdateOnly was specified. Exiting.." + return + } + + # TODO: This doesn't handle the case of "the existing node is a value and the asked-for value is a complex object" + # Right now this just does nothing in that case, but it should error or warn + + if ($null -eq $tempNode.$node) { + # If this is not the last entry in the list, keep adding subobjects so we can add values + if ($node -ne $keyNodeList[-1]) { + Write-Host "$logLead : Could not find sub-element with key [$node]. Adding one so we can add sub-elements." + $tempNode | Add-Member -MemberType NoteProperty -Name $node -Value (New-Object -TypeName "PSCustomObject") + } else { + # Otherwise set it to the value that we wanted to add since we didn't have a value yet anyways + Write-Host "$logLead : Could not find appSetting Key [$node]. Creating one and setting value to [$Value]." + $tempNode | Add-Member -MemberType NoteProperty -Name $node -Value $Value + } + $dirty = $true + } else { + # if it's the last node in our list and the value doesn't match, set the value here + if ($node -eq $keyNodeList[-1]) { + if ($tempNode.$node -cne $Value) { + Write-Host "$logLead : Found appSetting [$node], changing value from [$($tempNode.$node)] to [$Value]." + $tempNode.$node = $Value + $dirty = $true + } + } else{ + if ($tempNode.$node.GetType().Name -ne "PSCustomObject") { + Write-Error "$logLead : Asked to add a subproperty [$node] to element [$lastNode] but the element [$lastNode] can not take properties" + return + } + } + } + + $tempNode = $tempNode.$node + $lastNode = $node + } + + # Only useful when debugging. Don't write typically. + $debugFinalValue = ConvertTo-Json -InputObject $json -Depth 100 -Compress:$true + Write-Host "$logLead [$debugFinalValue]" + + if($dirty) { + Write-Verbose "$logLead : Saving config to path [$FilePath]" + + if ($Force -or $PSCmdlet.ShouldProcess("Ready to write the file [$FilePath] contents?")) { + Set-Content -Path $FilePath -Value ($json | Format-Json) + } else { + Write-Host "$logLead : When confirmed, will write to file [$FilePath]" + } + } else { + Write-Verbose "$logLead : No value changes were made to $FilePath. Not updating the file." + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateXml.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateXml.ps1 new file mode 100644 index 0000000..037acf6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/Set-AppSettingPrivateXml.ps1 @@ -0,0 +1,165 @@ +function Set-AppSettingPrivateXml { +<# +.SYNOPSIS + Sets an appSetting Key/Value pair in the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Set an appSetting key/value pair in the specified config file. + Will default to the global 64 bit machine.config file if no config file value specified. + Will not tickle files where no values have changed. + Can connect to remote computers as well. + +.PARAMETER key + [string] The appSetting key + +.PARAMETER value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER filePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. + +.PARAMETER UpdateOnly + [switch] Only updates a key's value if it exists. Will not create the missing AppSetting key. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + param ( + [Parameter(Mandatory = $true)] + [string]$key, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$value, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$UpdateOnly + ) + + $logLead = (Get-LogLeadName) + $dirty = $false + + # We already ensured it exists because we are in the private function + $xml = (Read-XMLFile -FilePath $FilePath) + + ## Beware of the case of more than one appSettings element in case we run into issues, le sigh + ## Let's go ahead and let the person reading the logs know that there were more than one section + ## In the case of multiple found, we want to save the shortest-path to add our settings to in the future + ## We should track that as we calculate our paths + + $shortestAppSettingsPathSize = [int]::MaxValue # this keeps us having to do 0 checks as well + $appSettingsCollection = @($xml.SelectNodes('//appSettings')) + $preferredAppSettingsLocation = $appSettingsCollection[0] + + if ($appSettingsCollection.Count -gt 1) { + Write-Warning "$logLead : More than one appSettings section found to manipulate in [$FilePath]" + foreach($appSettingElement in $appSettingsCollection) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + if ($shortestAppSettingsPathSize -lt $fullPath.length) { + $preferredAppSettingsLocation = $appSettingElement + $shortestAppSettingsPathSize = $fullPath.length + } + Write-Host $fullPath + } + } + + if (Test-IsCollectionNullOrEmpty $appSettingsCollection) { + ## We don't have an appSettings element to work with + ## Let's add one and then store it for later use + Write-Warning "$logLead : No appSettings section found to manipulate in [$FilePath], creating one on element" + [void]$xml.SelectSingleNode("configuration").AppendChild($xml.CreateElement("appSettings")) + $preferredAppSettingsLocation = @($xml.SelectNodes('//appSettings'))[0] + $dirty = $true + } + + ## Now that we have a location to store any settings, let's go see if we have an existing value to update first + $existingSettingValuesByKey = @($xml.configuration.SelectNodes("//appSettings/add[@key='$key']")) + + ## Crap, we found more than one element with the same key. + ## We can't edit this, but honestly the system can't work with it this way either + ## Something will absolutely break, but it doesn't have to be us today + if ($existingSettingValuesByKey.length -gt 1) { + Write-Error "$logLead : Found too many nodes with the same Key value in [$FilePath]!!" + + foreach($appSettingElement in $existingSettingValuesByKey) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + Write-Host "$logLead : Duplicate node found at: $fullPath" + $appSettingElement.Value = $value + $dirty = $true + } + } + + # If the UpdateOnly switch is specified, and an AppSetting was not found, early out of saving anything. + if ($existingSettingValuesByKey.length -eq 0 -and $UpdateOnly) { + Write-Host "$logLead : Could not find appSetting key '$key'. Not creating AppSetting node because UpdateOnly was specified. Exiting.." + return + } + + ## Now we either have a value in $existingSettingValuesByKey or we don't + ## If we have one, we set that value to the requested value + ## If we don't have an element there, we create one and stuff it on $preferredAppSettingsLocation + ## We should track our dirty status so we only update the file if it has to be touched + ## This last part is important for web.config which will bounce a webapp + if ($existingSettingValuesByKey.length -eq 0) { + ## We don't have a value yet in the file, let's add it to that $preferredAppSettingsLocation we found before + Write-Host "$logLead : Could not find appSetting key '$key'. Creating one and setting value to '$value'." + + $dirty = $true + + $appSettingElement = $xml.CreateElement("add") + $appSettingElement.SetAttribute("key", $key) + $appSettingElement.SetAttribute("value", $value) + [void]$preferredAppSettingsLocation.AppendChild($appSettingElement) + } else { + ## We have the key already, let's update the value + $existingValue = @($existingSettingValuesByKey)[0].Value + + ## Ensure that if the case is changed that we for sure update the value (CNE compare) + if ($existingValue -cne $value) { + Write-Host "$logLead : Found appSetting '$key', changing value from '$existingValue' to '$value'." + $dirty = $true + } + @($existingSettingValuesByKey)[0].Value = $value + } + + if($dirty) { + Write-Verbose "$logLead : Saving config to path $FilePath" + + if ($Force -or $PSCmdlet.ShouldProcess("Ready to write the file [$FilePath] contents?")) { + ## Make sure the file gets saved with NoBOM + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + + Save-XMLFile $FilePath $xml.OuterXml $utfNoBOM + } else { + Write-Host "$logLead : When confirmed, will write to file [$FilePath] with contents [[$($xml.OuterXml)]]" + } + } else { + Write-Verbose "$logLead : No changes were made to $FilePath" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.Configuration/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..f1100db --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Private/VariableDeclarations.ps1 @@ -0,0 +1,26 @@ +# This can be modified if needed, but probably shouldn't be +$global:machineConfigPath = Get-DotNetConfigPath + +<# + +From Platform-A repo: PLAT\alkami.utilities\Alkami.Utilities\Configuration\EnvironmentType.cs + +public enum EnvironmentType +{ + Development = 10, + TeamQA = 20, + QA = 30, + Integration = 100, + Build = 110, + LoadTest = 120, + Sandbox = 121, + Regression = 122, + Test = 123, + SDK = 124, + Unconfigured = 500, + Secure = 1000, + Staging = 2000, + Production = 3000, + Unknown = 9999 +} +#> \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Add-EagleEyeConfig.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Add-EagleEyeConfig.ps1 new file mode 100644 index 0000000..4576898 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Add-EagleEyeConfig.ps1 @@ -0,0 +1,78 @@ +function Add-EagleEyeConfig{ +<# +.SYNOPSIS +Adds the Eagle eye configuration to the machine config the local machine. + +.DESCRIPTION +Gets the directory of the local machine config and uses System.Xml.XmlNode class +to add the configuration values for Eagle eye. + +.PARAMETER use64bit +Boolean, passed to Get-DotNetConfigPath - if set to true, will return the filepath +of the 64 bit machine config file. + +.EXAMPLE +Add-EagleEyeConfig +Base usage, will add the configuration group and section for eagle eye. + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$False)] + [bool]$use64Bit = $true + ) + begin{ + $logLead = (Get-LogLeadName); + + Write-Verbose "$logLead Getting Maching.Config path, use64Bit set to $use64Bit" + $machineConfigPath = Get-DotNetConfigPath -use64Bit $use64Bit + if(!$machineConfigPath){throw "Machine config path could not be found"} + + Write-Verbose "$logLead Reading machine config file located at $machineConfigPath" + $machineConfig = Read-XMLFile $machineConfigPath + if(!$machineConfig){throw "Machine config at $machineConfigPath could not be converted to xml"} + + Write-Verbose "$logLead Initializing eagle eye configuration xml" + $eagleEyeConfigXmlString = ' + + + + + + + + ' + $eagleEyeSectionXmlString = '
' + } + process{ + + Write-Verbose "$logLead Ensuring configuration and configSection nodes exist" + if(!$machineConfig.configuration){ + [void]$machineConfig.AppendChild($machineConfig.CreateNode("element","configuration", $null)) + } + if(!$machineConfig.configuration.configSections){ + [void]$machineConfig.SelectSingleNode("configuration").AppendChild($machineConfig.CreateElement("configSections")) + } + + Write-Verbose "$logLead Adding eagle eye config to configuration node." + if(!$machineConfig.configuration.authorizedGroupsByOperation){ + $eagleEyConfigDoc = [xml]($eagleEyeConfigXmlString) + $eagleEyeNode = $machineConfig.ImportNode($eagleEyConfigDoc.FirstChild, $true) + [void]$machineConfig.configuration.AppendChild($eagleEyeNode) + } + Write-Verbose "$logLead Adding eagle eye section group to configSections" + if(!($machineConfig.configuration.configSections.section | Where-Object {$_.Name -eq "authorizedGroupsByOperation"})){ + $eagleEyeSection = [xml]($eagleEyeSectionXmlString) + $eagleEyeSectionNode = $machineConfig.ImportNode($eagleEyeSection.FirstChild, $true) + [void]$machineConfig.configuration.SelectSingleNode("configSections").AppendChild($eagleEyeSectionNode) + } + + Write-Verbose "$logLead Saving config file to path $machineConfigPath" + $machineConfig.Save($machineConfigPath) + + Write-Verbose "$logLead Finished" + } +} +# ToDo - Either move these to common if they add value or modify the manifest to not export them +#region Private functions + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Add-NetshExcludedPortRange.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Add-NetshExcludedPortRange.ps1 new file mode 100644 index 0000000..cb351b1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Add-NetshExcludedPortRange.ps1 @@ -0,0 +1,76 @@ +function Add-NetshExcludedPortRange { +<# +.SYNOPSIS + This function is used to add a specific starting and number of ports to the system. + This is a very thing wrapper around the netsh tool for adding IPv4 ports. + +.PARAMETER Start + This is the starting port number. + +.PARAMETER NumberOfPorts + This is the count of ports in the given range. + +.PARAMETER End + This is the end port number when providing a range. + +.EXAMPLE + Add-NetshExcludedPortRange -Start 50 -End 55 + +.EXAMPLE + Add-NetshExcludedPortRange -Start 50 -NumberOfPorts 6 +#> + [CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("StartPort")] + [int]$Start, + [Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')] + [ValidateNotNullOrEmpty()] + [Alias("Range")] + [int]$NumberOfPorts, + [Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')] + [ValidateNotNullOrEmpty()] + [Alias("EndPort")] + [int]$End + ) + + $logLead = (Get-LogLeadName) + + if ($Start -le 0) { + throw "$logLead : Start port value must be greater than 0" + } + + if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') { + if ($NumberOfPorts -eq $Start) { + throw "$logLead : NumberOfPorts can not equal the parameter for Start" + } + + if ($NumberOfPorts -le 0) { + throw "$logLead : NumberOfPorts value must be greater than 0" + } + } + + if ($PSCmdlet.ParameterSetName -eq 'EndPorts') { + if ($End -le $Start) { + throw "$logLead : End port value must be larger than Start port value" + } + + if ($End -le 0) { + throw "$logLead : End port value must be greater than 0" + } + + $NumberOfPorts = $End - $Start + 1 + } + + Write-Host "$logLead : Creating ipv4/tcp excludedPortRange for Start port [$Start] for [$NumberOfPorts] ports" + $output = netsh int ipv4 Add excludedportrange protocol=tcp startport=$Start numberofports=$NumberOfPorts + + if ($output -match "error") { + Write-Error "$logLead : Failed to add port range`r`n$output" + return $false + } + + return $true +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.ps1 new file mode 100644 index 0000000..b594b40 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.ps1 @@ -0,0 +1,45 @@ +function Copy-AlkamiLog4net { +<# +.SYNOPSIS + Renames staged log4net.config files to new.log4net.config if there isn't already one in the build output + Deletes staged log4net.config files if there's already a new.log4net.config file in the build output + +.NOTES + I'm not a fan of this verb but I don't know everywhere this function might be called so I'm leaving it +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$TempPath = "C:\temp\deploy\" + ) + + $logLead = Get-LogLeadName + + Write-Host ("$logLead : Looking for Subfolders Under Directory {0}" -f $TempPath) + $orbFolders = Get-ChildItem $TempPath | Where-Object { $_.PsIsContainer } | Select-Object -ExpandProperty FullName + + foreach ($orbFolder in $orbFolders) { + + $log4netConfig = Get-ChildItem $orbFolder | Where-Object { $_.Name -eq "log4net.config" } | Select-Object -ExpandProperty FullName + + if ($null -ne $log4netConfig -and $log4netConfig.Count -gt 0) { + + $newLogConfig = Join-Path (Split-Path $log4netConfig) "new.log4net.config" + + if (Test-Path $newLogConfig) { + + Write-Host ("$logLead : Found file {0} -- deleting {1}" -f $newLogConfig, $log4netConfig) + Remove-Item $log4netConfig -Force + } else { + + Write-Host ("$logLead : Could not find {0} -- renaming {1}" -f $newLogConfig, $log4netConfig) + Move-Item $log4netConfig $newLogConfig -Force + } + } else { + + # Not a warning because we don't actually care + Write-Host ("No log4net.config found under {0}" -f $orbFolder) + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.tests.ps1 new file mode 100644 index 0000000..490b5e8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Copy-AlkamiLog4net.tests.ps1 @@ -0,0 +1,60 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Copy-AlkamiLog4net + +Describe Copy-AlkamiLog4net { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Warning ("Using temp path: $tempPath for tests") + + Context "Log4net Tests" { + + It "Renames the Log4net Config When No new.log4net.config Exists" { + + $bankPath = Join-Path $tempPath "Bank" + $bankFolder = New-Item -ItemType Directory $bankPath + $logConfigPath = Join-Path $bankFolder "log4net.config" + $newLogConfigPath = Join-Path $bankFolder "new.log4net.config" + + "" | Out-File $logConfigPath + + (Get-ChildItem $logConfigPath).Count | Should Be 1 + Get-ChildItem $newLogConfigPath -ErrorAction SilentlyContinue | Should Be $null + Copy-AlkamiLog4net $tempPath + Get-ChildItem $logConfigPath -ErrorAction SilentlyContinue | Should Be $null + (Get-ChildItem $newLogConfigPath).Count | Should Be 1 + } + + It "Deletes log4net.config When new.log4net.config exists" { + + $corePath = Join-Path $tempPath "Core" + $coreFolder = New-Item -ItemType Directory $corePath + $logConfigPath = Join-Path $corePath "log4net.config" + $newLogConfigPath = Join-Path $corePath "new.log4net.config" + + "" | Out-File $logConfigPath + "new config" | Out-File $newLogConfigPath + + (Get-ChildItem $logConfigPath).Count | Should Be 1 + (Get-ChildItem $newLogConfigPath ).Count | Should Be 1 + + Copy-AlkamiLog4net $tempPath + + Get-ChildItem $logConfigPath -ErrorAction SilentlyContinue | Should Be $null + (Get-ChildItem $newLogConfigPath).Count | Should Be 1 + (GC $newLogConfigPath) -match "new config" | Should Be $true + } + } +} + +#endregion Copy-AlkamiLog4net \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Find-CertificateBySubjectOrSAN.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Find-CertificateBySubjectOrSAN.ps1 new file mode 100644 index 0000000..3a04cd7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Find-CertificateBySubjectOrSAN.ps1 @@ -0,0 +1,119 @@ +function Find-CertificateBySubjectOrSAN { +<# +.SYNOPSIS + Searches for Certificates by Subject or Subject Alternate Name + Additionally searches for a "wildcard" match, such as *.dev.alkamitech.com + This replaces [Alkami.Ops.Common.Cryptography.CertificateHelper]::FindCertificatebySubjectOrSAN but without searching on a remote server + +.PARAMETER SubjectOrSAN + A Subject name or Subject Alternative Name + Typically this is the domain name you want to secure + +.PARAMETER StoreLocation + [System.Security.Cryptography.X509Certificates.StoreLocation] The certificate location to search (defaults to LocalMachine) + +.PARAMETER StoreName + [System.Security.Cryptography.X509Certificates.StoreName] The certificate store to search in (defaults to My - aka Personal) + +.OUTPUTS + Either an [System.Security.Cryptography.X509Certificates.X509Certificate2] certificate or null +#> + [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] + param ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [Alias('Subject')] + [Alias('SAN')] + [Alias('Name')] + [string]$SubjectOrSAN, + [Parameter()] + [System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine, + [Parameter()] + [System.Security.Cryptography.X509Certificates.StoreName]$StoreName = [System.Security.Cryptography.X509Certificates.StoreName]::My + ) + + $logLead = Get-LogLeadName + + Write-Host "$logLead : Looking for certificate for [$SubjectOrSAN]" + + $segments = $SubjectOrSAN.Split('.') + $segments[0] = "*" + $wildcardUri = [string]::Join(".", $segments) + + # If all we got is a *, we won't search for wildcards, since it would be unreliable + $searchWildCard = $wildcardUri -ne "*" + if (-not $searchWildCard) { + Write-Host "$logLead : Won't search for the wildcard pattern of '*'. Will still search for a specific SAN tho." + } + + # Assume we aren't going to a remote machine for this function + $certificates = Get-AllCertificatesFromSpecificStore -StoreLocation $StoreLocation -StoreName $StoreName + + foreach ($certificate in $certificates) + { + try { + $subjectAlternateNames = $certificate.Extensions.Where({ $_.OID.FriendlyName -eq 'Subject Alternative Name' }) + # Don't test against $null alone, because some have an empty collection being returned + if (-not (Test-IsCollectionNullOrEmpty -Collection $subjectAlternateNames)) { + $asnData = [System.Security.Cryptography.AsnEncodedData]::new($subjectAlternateNames.Oid, $subjectAlternateNames.RawData) + + $asnDataContainsSubjectOrSAN = $asnData.Format($true).IndexOf($SubjectOrSAN, [StringComparison]::OrdinalIgnoreCase) -ge 0 + if ($asnDataContainsSubjectOrSAN) { + Write-Host "$logLead : Found a matching SAN for [$SubjectOrSAN]" + } + + $asnDataContainsWildcard = $false + if ($searchWildCard) { + $asnDataContainsWildcard = $asnData.Format($true).IndexOf($wildcardUri, [StringComparison]::OrdinalIgnoreCase) -ge 0 + if ($asnDataContainsWildcard) { + Write-Host "$logLead : Found a matching SAN with wildcard for [$wildcardUri]" + } + } + + $validCertificate = $asnDataContainsSubjectOrSAN -or $asnDataContainsWildcard + if ($validCertificate) { + Write-Host "$logLead : Found and returning certificate with thumbprint [$($certificate.Thumbprint)]" + return $certificate + } + } + } catch { + # not writing the exception here because we don't care. We know this happens with a handful of certificates and the one we want it will succeed for. + # I would Write-Verbose but that's really hard to debug on servers, so ... + Write-Host "$logLead : Could not parse the SubjectAlternateNames for the cert with thumbprint [$($certificate.Thumbprint)] in [$StoreLocation\$StoreName]" + } + + Write-Host "$logLead : No certificates found by SAN, falling back to Subject lookup" + + try { + $subject = $certificate.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) + if ($null -ne $subject) { + $subjectContainsSubjectOrSAN = $subject.IndexOf($SubjectOrSAN, [StringComparison]::OrdinalIgnoreCase) -ge 0 + if ($subjectContainsSubjectOrSAN) { + Write-Host "$logLead : Found a matching Subject for [$SubjectOrSAN]" + } + + $subjectContainsWildcard = $false + if ($searchWildCard) { + $subjectContainsWildcard = $subject.IndexOf($wildcardUri, [StringComparison]::OrdinalIgnoreCase) -ge 0 + if ($subjectContainsWildcard) { + Write-Host "$logLead : Found a matching Subject with wildcard for [$wildcardUri]" + } + } + + $validCertificate = $subjectContainsSubjectOrSAN -or $subjectContainsWildcard + if ($validCertificate) { + Write-Host "$logLead : Found and returning certificate with thumbprint [$($certificate.Thumbprint)]" + return $certificate + } + } + } catch { + # not writing the exception here because we don't care. We know this happens with a handful of certificates and the one we want it will succeed for. + # I would Write-Verbose but that's really hard to debug on servers, so ... + Write-Host "$logLead : Could not parse the subject lookup information for the cert with thumbprint [$($certificate.Thumbprint)] in [$StoreLocation\$StoreName]" + } + } + + Write-Host "$logLead : No matching certificate found, returning null" + return $null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.ps1 new file mode 100644 index 0000000..1dd8a91 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.ps1 @@ -0,0 +1,92 @@ +function Get-AllAppSettingKeys { +<# +.SYNOPSIS + Returns an all appsetting keys from the specified file. Filepath defaults to the 64 bit machine config. + +.PARAMETER filePath + [string] The location of the specific config file to check in + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +.OUTPUTS + Returns an array (empty as appropriate) +#> + [CmdletBinding()] + [OutputType([string[]])] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName) + +#region guard clauses + # If a computername was provided, modify the filepath to be a UNC path. + if((![string]::IsNullOrWhiteSpace($ComputerName)) -and ($ComputerName -ne "localhost")) { + $FilePath = (Get-UncPath -filePath $FilePath -ComputerName $ComputerName) + } + + if (!(Test-Path -PathType Leaf -Path $FilePath)) { + Write-Warning "$logLead : Could not find a file at $FilePath. Execution cannot continue. Returning `$null" + return $null + } + + Write-Verbose "$logLead : Reading Config file at $FilePath"; + $xml = Read-XMLFile $FilePath + if(!$xml) { + throw "$logLead : Config at $FilePath could not be converted to xml." + } + + Write-Verbose "$logLead : Ensuring configuration root node exists..."; + if(!$xml.configuration){ + throw "$logLead : How does $FilePath not have a root element??" + } +#endregion guard clauses + + $allKeys = @() + + ## Beware of the case of more than one appSettings element in case we run into issues, le sigh + ## Let's go ahead and let the person reading the logs know that there were more than one section + + $appSettingsCollection = @($xml.SelectNodes('//appSettings')) + + if ($appSettingsCollection.Count -gt 1) { + Write-Warning "$logLead : More than one appSettings section found to manipulate in [$FilePath]" + foreach($appSettingElement in $appSettingsCollection) { + $fullPath = $appSettingElement.Name + $parentElement = $appSettingElement.ParentNode + while ($null -ne $parentElement) { + if($parentElement.Path) { + $fullPath = $parentElement.Name + '[path=' + $parentElement.Path + ']/' + $fullPath + } else { + $fullPath = $parentElement.Name + '/' + $fullPath + } + $parentElement = $parentElement.ParentNode + } + Write-Host "Too many appSettings: $fullPath" + } + } + + if ($appSettingsCollection.Count -eq 0) { + ## We don't have an appSettings element to work with + ## Announce that, then return $null + + Write-Warning "$logLead : Could not find an appSettings element at all in [$FilePath]. Returning an empty array." + return $allKeys + } + + ## Now that we have at least one location for settings, let's go see if we have an existing value + $appSettingsAdds = @($xml.configuration.SelectNodes("//appSettings/add")) + + foreach($appSettingElement in $appSettingsAdds) { + $allKeys += $appSettingElement.Key + } + + return $allKeys +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.tests.ps1 new file mode 100644 index 0000000..11f5ca0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllAppSettingKeys.tests.ps1 @@ -0,0 +1,115 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AllAppSettingKeys" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + $fakeConfigFile = Join-Path $tempPath "fake.config" + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Host ("Using temp path: $tempPath for tests") + + Context "Returns all keys when two appSettings sections are present" { + + $appSettingsTestContents = @" + + + + + + + + + + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + $value = Get-AllAppSettingKeys -Path $fakeConfigFile + + It "still got the right value" { + $value | Should -Be @("TestKey1","TestKey2","TestKey1","TestKey2") + } + } + + Context "Returns all keys when one appsetting section is present" { + + $appSettingsTestContents = @" + + + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + $value = Get-AllAppSettingKeys -Path $fakeConfigFile + + It "still got the right value" { + $value | Should -Be @("TestKey1","TestKey2") + } + } + + Context "Returns empty array if the appSettings Node Doesn't Exist" { + + $appSettingsTestContents = @" + + + + + +"@ + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + $value = Get-AllAppSettingKeys -Path $fakeConfigFile + + It "still got an empty array" { + $value | Should -HaveCount 0 + } + } + + Context "Throws an error if the configuration Node doesn't exist" { + + $appSettingsTestContents = @" +"@ + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + $value = $null + + {$value = Get-AllAppSettingKeys -Path $fakeConfigFile} | Should -Throw + } + + Context "Does a happy path" { + + $appSettingsTestContents = @" + + + + + +"@ + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + $value = Get-AllAppSettingKeys -Path $fakeConfigFile + + It "got the right value" { + $value | Should -Be @('FakeSetting') + } + } + + Remove-Item $fakeConfigFile -Force + Remove-Item $tempPath -Force +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AllCertificatesFromSpecificStore.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllCertificatesFromSpecificStore.ps1 new file mode 100644 index 0000000..e0e52e3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AllCertificatesFromSpecificStore.ps1 @@ -0,0 +1,17 @@ +function Get-AllCertificatesFromSpecificStore { +<# +.SYNOPSIS + Thin veneer wrapper around Get-ChildItem on the cert store. + Useful for mocking/testing more than calling gci directly. +#> + [CmdletBinding()] + [OutputType([Object])] + param ( + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation, + [Parameter(Mandatory = $true)] + [System.Security.Cryptography.X509Certificates.StoreName]$StoreName + ) + + return @(Get-ChildItem Cert:\$StoreLocation\$StoreName) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.ps1 new file mode 100644 index 0000000..64b0297 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.ps1 @@ -0,0 +1,108 @@ +function Get-AppServiceAccountName { +<# +.SYNOPSIS + This function gets the domain qualified gMSA name + +.DESCRIPTION + This function gets the domain qualified gMSA service account name. + This function can throw an error if the service name doesn't match the predefined list. + This will return an empty string if the UserPrefix environment variable isn't set. + This will return an empty string if there is no domain on the current machine. + If an empty string is returned, the expectation is that this will be installed local-machine-style (a-la SDK environments). + +.PARAMETER ServiceName + [string] A known service name, such as BankService + +.INPUTS + Requires the ServiceName to be passed in + +.OUTPUTS + Will return the domain-app-specific username, an empty string (if the domain/userprefix aren't set, such as an SDK install), or throws an error when mixed conditions are found. + +.EXAMPLE + Get-AppServiceAccountName + +This will throw an error for no account name passed in + +.EXAMPLE + Get-AppServiceAccountName -ServiceName RandomNonsense + +This will throw an error for a bad service name. + +Get-AppServiceAccountName -ServiceName RandomNonsense + +WARNING: Could not find a matching entry in the lookup matrix for [RandomNonsense] +Could not find a matching entry in the lookup matrix for [RandomNonsense] +At line:X char:13 ++ throw $message ++ ~~~~~~~~~~~~~~ + + CategoryInfo : OperationStopped: (Could not find ...RandomNonsense]:String) [], RuntimeException + + FullyQualifiedErrorId : Could not find a matching entry in the lookup matrix for [RandomNonsense] + +.EXAMPLE + Get-AppServiceAccountName -ServiceName BankService + +Get-AppServiceAccountName -ServiceName BankService + +corp\dev.bank$ +#> + [CmdletBinding()] + [OutputType([System.String])] + param( + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) + process { + $logLead = (Get-LogLeadName) + + $domain = (((Get-CimInstance Win32_ComputerSystem).Domain) -split '\.')[0] + + if ([string]::IsNullOrWhiteSpace($domain)) { + Write-Warning "$logLead : Could not find the local machine domain name. Are you joined to a domain?" + Write-Verbose "$logLead : Assuming the user is on an SDK machine (not connected to a domain, can't use gMSA. Returning empty-string." + return "" + } + + $LookupMatrix = @{ + 'AuditService' = 'audit'; + 'BankService' = 'bank'; + 'ContentService' = 'content'; + 'CoreService' = 'core'; + 'ExceptionService' = 'exception'; + 'MessageCenterService' = 'msgctr'; + 'NagConfigurationService' = 'nag'; + 'NotificationService' = 'notify'; + 'RP-STS' = 'rpsts'; + 'SchedulerService' = 'schedule'; + 'SecurityManagementService' = 'secmgr'; + 'STSConfiguration' = 'stsconf'; + 'SymConnectMultiplexer' = 'multiplx'; + 'Alkami Radium Scheduler Service' = 'radium'; + 'Alkami Nag Service' = 'nag'; + } + + $matrixValue = $LookupMatrix[$ServiceName] + + if ([string]::IsNullOrWhiteSpace($matrixValue)) { + $message = "$logLead : Could not find a matching entry in the lookup matrix for [$ServiceName]" + Write-Warning $message + } + + $userPrefix = (Get-AppSetting -appSettingKey "Environment.UserPrefix") + + ## This is so we can use this as ($env:userdnsdomain)\(Get-AppSetting "Environment.UserPrefix").$MatrixLookup[appName]$ + + if ([string]::IsNullOrEmpty($userPrefix) -or [string]::IsNullOrEmpty($matrixValue)) { + if (Test-IsAppServer) { + ## If we don't have a configured value then let's just run everything as the dbms user + ## This is non-ideal of course, but we haven't got the infrastructure yet to fix it + ## TODO: @dsage - Where do we get the user prefix for (ex: corp\dev.bank$ so we need dev) from? + return (Get-AppSetting -appSettingKey "DatabaseMicroServiceAccount") + } + Write-Verbose "$logLead : No user prefix (ex: dev, qa, prod) found on this machine. We can't build the user string from here. Defaulting to empty string so that we use local machine configuration. (see SDK users)" + return "" + } + + return "$domain\$userPrefix.$matrixValue`$" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.tests.ps1 new file mode 100644 index 0000000..d440f88 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppServiceAccountName.tests.ps1 @@ -0,0 +1,111 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AppServiceAccountName" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return "sb" } + + Context "Testing basic values with domain and is-app-server" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $true } + + Mock -ModuleName $moduleForMock -CommandName Get-CimInstance -MockWith { return @{ Domain = "Test.Domain"; } } + + It "Testing known matrix value" { + Get-AppServiceAccountName "AuditService" | Should Be "test\sb.audit$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "BankService" | Should Be "test\sb.bank$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "ContentService" | Should Be "test\sb.content$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "CoreService" | Should Be "test\sb.core$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "ExceptionService" | Should Be "test\sb.exception$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "MessageCenterService" | Should Be "test\sb.msgctr$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "NagConfigurationService" | Should Be "test\sb.nag$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "NotificationService" | Should Be "test\sb.notify$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "RP-STS" | Should Be "test\sb.rpsts$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "SchedulerService" | Should Be "test\sb.schedule$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "SecurityManagementService" | Should Be "test\sb.secmgr$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "STSConfiguration" | Should Be "test\sb.stsconf$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "SymConnectMultiplexer" | Should Be "test\sb.multiplx$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "Alkami Radium Scheduler Service" | Should Be "test\sb.radium$" + } + + It "Testing known matrix value" { + Get-AppServiceAccountName "Alkami Nag Service" | Should Be "test\sb.nag$" + } + + It "Testing unknown matrix value" { + Get-AppServiceAccountName "BankService2" | Should Be "sb" + } + } + + Context "Testing basic values with domain and is-not-app-server" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + + Mock -ModuleName $moduleForMock -CommandName Get-CimInstance -MockWith { return @{ Domain = "Test.Domain"; } } + + It "Testing known matrix value" { + Get-AppServiceAccountName "BankService" | Should Be "test\sb.bank$" + } + It "Testing unknown matrix value" { + Get-AppServiceAccountName "BankService2" | Should Be "" + } + } + + Context "Testing basic values without domain" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer -MockWith { return $false } + + Mock -ModuleName $moduleForMock -CommandName Get-CimInstance -MockWith { return @{ Domain = ""; } } + + It "Testing known matrix value" { + Get-AppServiceAccountName "BankService" | Should Be "" + } + It "Testing unknown matrix value" { + Get-AppServiceAccountName "BankService2" | Should Be "" + } + } +} + +#endregion Get-ConfigurationFiles \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.json.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.json.tests.ps1 new file mode 100644 index 0000000..ddeb8ec --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.json.tests.ps1 @@ -0,0 +1,128 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.json\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Get-Module $sut | Remove-Module -Force +$moduleForMock = "Alkami.PowerShell.Configuration" + +Describe "Get-AppSetting.json" { + $configPathUUT = "TestDrive:\appsettings.json" + $configContentUUT = @" +{ + "SettingName" : "SettingValue", + "DeepSettingParent" : { + "DeepSettingChild": "DeepSettingValue" + }, + "ReallyDeepSettingParent" : { + "DeepSettingParent" : { + "DeepSettingChild": "DeepSettingValue" + } + } +} +"@ + + Context "Simple_Setting" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Set-Content -Path $configPathUUT -Value $configContentUUT + + $value = Get-AppSetting -Path $configPathUUT -Key "SettingName" + + It "got the right value" { + $value | Should -Be "SettingValue" + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote no warnings to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + + It "wrote no hosts messages" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } + + Context "DeepSettingParent.RightName" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Set-Content -Path $configPathUUT -Value $configContentUUT + + $value = Get-AppSetting -Path $configPathUUT -Key "DeepSettingParent:DeepSettingChild" + + It "got the right value" { + $value | Should -Be "DeepSettingValue" + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote no warnings to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + + It "wrote no hosts messages" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } + + Context "DeepSettingParent.WrongName" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Set-Content -Path $configPathUUT -Value $configContentUUT + + $value = Get-AppSetting -Path $configPathUUT -Key "DeepSettingParent:SettingName" + + It "got the right value" { + $value | Should -BeNull + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote a simple warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "wrote no hosts messages" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } + + Context "ReallyDeepSettingParent" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Set-Content -Path $configPathUUT -Value $configContentUUT + + $value = Get-AppSetting -Path $configPathUUT -Key "ReallyDeepSettingParent:DeepSettingParent:SettingName" + + It "got the right value" { + $value | Should -BeNull + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote a simple warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "wrote no hosts messages" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.ps1 new file mode 100644 index 0000000..99a20ca --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.ps1 @@ -0,0 +1,120 @@ +function Get-AppSetting { +<# + +.SYNOPSIS + Returns an appSetting value from the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Returns an appSetting value from the specified file for the given key. + Will default to the global 64 bit machine.config file if no config file value specified. + Can connect to remote computers as well. + +.PARAMETER key + [string] The name of the key to get the value from, if it exists. + +.PARAMETER filePath + [string] The location of the specific config file to check in + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +.PARAMETER SuppressWarnings + [switch] Suppress warnings about keys not found + +.OUTPUTS + Can return $null if key not found +#> + [CmdletBinding(DefaultParameterSetName = 'ByFile')] + param ( + [Parameter(ParameterSetName = 'ByFile', Mandatory = $true, Position = 0)] + [Parameter(ParameterSetName = 'ByXml', Mandatory = $true, Position = 0)] + [Alias("appSettingKey")] + [string]$Key, + + [Parameter(ParameterSetName = 'ByFile', Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(ParameterSetName = 'ByFile', Mandatory = $false)] + [string]$ComputerName = "localhost", + + [Parameter(ParameterSetName = 'ByXml', Mandatory = $false)] + [Xml]$XmlDocument = $null, + + [Parameter(ParameterSetName = 'ByFile', Mandatory = $false)] + [Parameter(ParameterSetName = 'ByXml', Mandatory = $false)] + [switch]$SuppressWarnings + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($Key)) { + throw "$logLead : Provided Key value [$Key] must be provided and non-empty/non-whitespace" + } + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + if ($PSCmdlet.ParameterSetName -eq 'ByFile') { + if(!(Compare-StringToLocalMachineIdentifiers -StringToCheck $ComputerName)) { + $FilePath = (Get-UncPath -filePath $FilePath -ComputerName $ComputerName) + } + } + + if (!(Test-Path -Path $FilePath)) { + Write-Warning "$logLead : Could not find a file at [$FilePath]. Execution cannot continue." + return ## exit early + } + + Write-Verbose "$logLead : Reading Config file at [$FilePath]"; + $isLikelyXml = ($null -ne $XmlDocument) #becomes true if we gave an xmldoc + $isLikelyJson = $false + $fileContent = $null + + # If we started off with xmldoc already, no need to do this, we can just crack on + if (!$isLikelyXml) { + $fileContent = Get-Content -Path $FilePath -Raw + $firstCharacter = $fileContent[0] + + if ($firstCharacter -eq '<') { + $isLikelyXml = $true + } + + if (($firstCharacter -eq '{') -or ($firstCharacter -eq '[')) { + $isLikelyJson = $true + } + } + + if ($isLikelyXml) { + if ($null -eq $XmlDocument) { + $XmlDocument = [xml]$fileContent + } + + if($null -eq $XmlDocument) { + throw "$logLead : Config at [$FilePath] expected to be xml but could not be parsed as xml." + } + + Write-Verbose "$logLead : Ensuring configuration root node exists..."; + if(!$XmlDocument.configuration){ + throw "$logLead : How does $FilePath not have a root element??" + } + + return Get-AppSettingPrivateXml -Key $Key -FileContent $XmlDocument -SuppressWarnings:$SuppressWarnings + } + + if ($isLikelyJson) { + try { + # See if we can parse this as a json object + (ConvertFrom-Json -InputObject $fileContent) | Out-Null + } catch { + Write-Host "$logLead : $($_.Exception.Message)" + throw "$logLead : Config at [$FilePath] expected to be json but could not be parsed as json." + } + + return Get-AppSettingPrivateJson -Key $Key -FileContent $fileContent -SuppressWarnings:$SuppressWarnings + } + + throw "$logLead : Could not parse this file [$FilePath]" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.tests.ps1 new file mode 100644 index 0000000..e459071 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.tests.ps1 @@ -0,0 +1,45 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force + +$moduleForMock = "" + +Describe "Get-AppSetting - base" { + $fakeConfigFile = "TestDrive:\doesnt_matter\fake.config" + + Context "Throws an error if the configuration Node doesn't exist" { + + $appSettingsTestContents = @" +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = $null + + #TODO - Test the inverse... if there's a good value here, does the ScriptBlock context actually + # set a value to $value? + {$value = Get-AppSetting -Path $fakeConfigFile -Key "FakeSetting"} | Should -Not -Throw + + It "should not have a value" { + $value | Should -BeNull + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote a warning to the console for missed content" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "wrote no other messages" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.xml.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.xml.tests.ps1 new file mode 100644 index 0000000..e68b9d4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-AppSetting.xml.tests.ps1 @@ -0,0 +1,221 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.xml\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Get-Module $sut | Remove-Module -Force + +$moduleForMock = "Alkami.PowerShell.Configuration" + +Describe "Get-AppSetting.xml" { + + $fakeConfigFile = "TestDrive:\fake.config" + + Context "Reports an error when two appSettings sections are present" { + + $appSettingsTestContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ + Set-Content -Path $fakeConfigFile -Value $appSettingsTestContents + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Get-AppSetting -Path $fakeConfigFile -Key "aspnet:MaxJsonDeserializerMembers" + + It "still got the right value" { + $value | Should -Be "60000" + } + + It "did throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + + It "wrote two warnings to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 2 -Scope Context + } + + It "wrote four found paths" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 4 -Scope Context + } + } + + Context "Writes a Warning and Returns Null if the Key Doesn't Exist" { + + $appSettingsTestContents = @" + + + + + +"@ + Set-Content -Path $fakeConfigFile -Value $appSettingsTestContents + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Get-AppSetting -Path $fakeConfigFile -Key "Not a real key value" + + It "still got the right value (null)" { + $value | Should -BeNull + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + + It "Returns the App Setting if the key is correct" { + $value = Get-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + $value | Should Be "FakeValue" + } + } + + Context "Writes a Warning and Returns the Last Value if More than One Setting Exists" { + + $appSettingsTestContents = @" + + + + + + +"@ + Set-Content -Path $fakeConfigFile -Value $appSettingsTestContents + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Get-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "still got the right value" { + $value | Should -Be "FakeValueDupe" + } + + It "did show an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "wrote two paths" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 2 -Scope Context + } + } + + Context "Writes a Warning and Returns Null if the appSettings Node Doesn't Exist" { + #TODO: These pass for the wrong reasons. If the file doesn't exist, these all pass. + # Change "Test-Path" mock to return $false to see + $appSettingsTestContents = @" + + + + + +"@ + Set-Content -Path $fakeConfigFile -Value $appSettingsTestContents + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Get-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "still got the right value (null)" { + $value | Should -BeNull + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } + + Context "Does a happy path" { + + $appSettingsTestContents = @" + + + + + +"@ + Set-Content -Path $fakeConfigFile -Value $appSettingsTestContents + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Get-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "still got the right value" { + $value | Should -Be 'FakeValue' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + + It "wrote no warnings to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Times 0 -Scope Context + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.ps1 new file mode 100644 index 0000000..93f3684 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.ps1 @@ -0,0 +1,23 @@ +function Get-ConfigSetting { +<# +.SYNOPSIS + This function will get the config setting on the machine starting with the machine.config, and progressing to the environment variables +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [Alias("Name")] + [string]$Key + ) + + $logLead = (Get-LogLeadName) + + $settingValue = (Get-AppSetting -Key $Key) + + if ($null -eq $settingValue) { + Write-Verbose "$logLead : Setting not found in machine config, trying environment variables" + $settingValue = (Get-EnvironmentVariable -Key $Key) + } + + return $settingValue +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.tests.ps1 new file mode 100644 index 0000000..e9f824f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigSetting.tests.ps1 @@ -0,0 +1,72 @@ +# . $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$originalMachineName = $env:COMPUTERNAME + +Describe "Get-ConfigSetting" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "SUT"} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + + Context "No Config Setting present returns `$null" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentVariable -MockWith { return $null } + + $response = Get-ConfigSetting "anything" + + It "should be `$null" { + $response | Should -BeNull + } + + It "tries to get the app setting" { + Assert-MockCalled -CommandName Get-AppSetting -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context + } + + It "tries to get the environment variable" { + Assert-MockCalled -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context + } + } + + Context "Environment Variable Setting present" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentVariable -MockWith { return "a value" } + + $response = Get-ConfigSetting "anything" + + It "should be the target value" { + $response | Should -Be "a value" + } + + It "tries to get the app setting" { + Assert-MockCalled -CommandName Get-AppSetting -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context + } + + It "tries to get the environment variable" { + Assert-MockCalled -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context + } + } + + Context "Machine Config Setting present" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return "a value" } + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentVariable -MockWith { return $null } + + $response = Get-ConfigSetting "anything" + + It "should be the target value" { + $response | Should -Be "a value" + } + + It "tries to get the app setting" { + Assert-MockCalled -CommandName Get-AppSetting -ModuleName $moduleForMock -Times 1 -Exactly -Scope Context + } + + It "does not try to get the environment variable" { + Assert-MockCalled -CommandName Get-EnvironmentVariable -ModuleName $moduleForMock -Times 0 -Exactly -Scope Context + } + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.ps1 new file mode 100644 index 0000000..7083927 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.ps1 @@ -0,0 +1,43 @@ +function Get-ConfigurationFiles { +<# +.SYNOPSIS + Pulls an Array of Configuration Files Which Need Updates +.PARAMETER stagedFilePath + + The base path to search for config files in, such as C:\ORB or C:\Temp\Deploy\ORB +.PARAMETER findTempFiles + + Defaults to $false. If set to $true, looks for new.*.config instead of *.config. Used when looking for the temporary + config files which come out of the build process +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$stagedFilePath, + [Parameter(Mandatory=$false)] + [bool]$findTempFiles = $false + ) + + $logLead = (Get-LogLeadName); + + Write-Host ("$logLead : Finding Configuration Files in Non-Symlink Folders Under {0}" -f $stagedFilePath) + $tempOrbfolders = Get-ChildItem -Directory $stagedFilePath | ` + Where-Object { $_.Attributes -notmatch "ReparsePoint" -and $_.Name -ne "Shared" } | ` + Select-Object -ExpandProperty FullName + + # Get-ChildItem is a bit finicky. We're going to trim \ from the end, then add \*, to use Include/Exclude without recursion + $tempOrbFolders = $tempOrbFolders | ForEach-Object { $_.TrimEnd("\") | ForEach-Object {$_ + "\*"}} + + if ($findTempFiles) + { + $configfiles = Get-ChildItem $tempOrbfolders -Include "new.*.config" -Exclude "new.log4net.config" | Select-Object -ExpandProperty FullName + } + else + { + $configfiles = Get-ChildItem $tempOrbfolders -Include "*.config" -Exclude "log4net.config","new*.config" | Select-Object -ExpandProperty FullName + } + + Write-Verbose ("$logLead : Found {0} Configuration Files" -f $configFiles.Count) + return $configFiles +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.tests.ps1 new file mode 100644 index 0000000..ee6a7d2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ConfigurationFiles.tests.ps1 @@ -0,0 +1,50 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-ConfigurationFiles + +Describe "Get-ConfigurationFiles" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + $tempOrb = ($tempPath + "\ORB") + + if (!(Test-Path $tempOrb)) + { + New-Item -ItemType Directory $tempOrb | Out-Null + } + + Write-Warning ("Using temp path: $tempOrb for tests") + + # Make Directories for Tests + $bankServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\BankService") + $coreServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\CoreService") + + # Make Files for Tests + "" | Out-File (Join-Path $bankServiceDirectory "new.web.config") + "" | Out-File (Join-Path $bankServiceDirectory "web.config") + "" | Out-File (Join-Path $coreServiceDirectory "new.web.config") + "" | Out-File (Join-Path $coreServiceDirectory "web.config") + + It "Filters temporary files from the build by default" { + + $testResults = Get-ConfigurationFiles $tempOrb + ($testResults | Where-Object {$_ -match "new\."}).Count | Should Be 0 + } + + It "Includes only tepmorary files from the build when specified" { + + $testResults = Get-ConfigurationFiles $tempOrb $true + ($testResults | Where-Object {$_ -match "new\."}).Count | Should Be 2 + } +} + +#endregion Get-ConfigurationFiles \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentDesignation.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentDesignation.ps1 new file mode 100644 index 0000000..a4a4a37 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentDesignation.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentDesignation { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.Designation + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Designation" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentHosting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentHosting.ps1 new file mode 100644 index 0000000..1c652f1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentHosting.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentHosting { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.Hosting + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Hosting" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentName.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentName.ps1 new file mode 100644 index 0000000..49e9e50 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentName.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentName { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.Name + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Name" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentNameSafeDesignation.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentNameSafeDesignation.ps1 new file mode 100644 index 0000000..638407e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentNameSafeDesignation.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentNameSafeDesignation { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.NameSafeDesignation + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.NameSafeDesignation" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentServer.ps1 new file mode 100644 index 0000000..7355ab1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentServer.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentServer { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.Server + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Server" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentType.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentType.ps1 new file mode 100644 index 0000000..29f22f9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentType.ps1 @@ -0,0 +1,34 @@ +function Get-EnvironmentType { +<# +.SYNOPSIS + Get the environment type from the machine.config or appropriate alternate location. Returns $null if not found. + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $loglead = (Get-LogLeadName) + + $environmentType = (Get-AppSetting -Key 'Environment.Type' -ComputerName $ComputerName) + + if ([string]::IsNullOrWhiteSpace($environmentType)) { + if (Compare-StringToLocalMachineIdentifiers $ComputerName) { + $environmentTypeKeyName = "ALKAMI.SRE.ENVIRONMENT_TYPE" + $environmentType = (Get-EnvironmentVariable -Name $environmentTypeKeyName -StoreName Machine) + } else { + $environmentType = Invoke-Command -ComputerName $ComputerName -ScriptBlock { + $environmentTypeKeyName = "ALKAMI.SRE.ENVIRONMENT_TYPE" + return (Get-EnvironmentVariable -Name $environmentTypeKeyName -StoreName Machine) + } + } + } + + Write-Verbose "$loglead : found this value for `$environmentType [$environmentType] - Could be empty if not present. That is a valid condition." + + return $environmentType +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentUserPrefix.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentUserPrefix.ps1 new file mode 100644 index 0000000..455630e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-EnvironmentUserPrefix.ps1 @@ -0,0 +1,33 @@ +function Get-EnvironmentUserPrefix { +<# + +.SYNOPSIS + Wrapper for Get-AppSetting for Environment.UserPrefix + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.UserPrefix" + ComputerName = $ComputerName + } + + $appSetting = Get-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$appSetting]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.ps1 new file mode 100644 index 0000000..8d5a029 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.ps1 @@ -0,0 +1,81 @@ +function Get-HelmApplicationYamls { + <# + .SYNOPSIS + Returns the yamls of the argocd application and the helm values file. + + .PARAMETER RepoPath + The path to the gitops repository containing the helm environment definitions. + + .PARAMETER EnvironmentName + The name of the environment to update. + + .EXAMPLE + # This returns the yamls (app definition and values file) for the TDE environment + + $repoPath = ".\alkami.gitops.kubernetes" + $envName = "tde" + $yamls = Get-HelmDeploymentYamls -RepoPath $repoPath -EnvironmentName $envName -Packages -Verbose + + # TODO: These are no longer returned? + # Write the file path containing the app definition, and the name of the application. + Write-Host $yamls.applicationPath + Write-Host $yamls.application.metadata.name + + # Write the file path containing the values file, and the configured version of the accounts microservice. + Write-Host $yamls.valuesPath + Write-Host $yamls.values.alk-svc-rpc-iaccountservicecontract-v2.tag + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$EnvironmentName, + [Parameter(Mandatory = $false)] + [string]$ChildPath = "alkami-services/environments" + ) + + $logLead = Get-LogLeadName + + # Find the environment path + $environmentsParentPath = Join-Path -Path $RepoPath -ChildPath $ChildPath + $environmentPath = Join-Path -Path $environmentsParentPath -ChildPath $EnvironmentName + Write-Host "$logLead : EnvironmentPath: [$environmentPath]" + + if (-not (Test-Path -Path $environmentPath)) { + #NOTE: This might or might not be HALTING, depending on execution environment + # TODO: What would that halting condition, and why would it not halt? + + # While we don't normally do TC messages in Module files, this seems appropriate per conversations and use-case + $errorMessage = "Could not find $EnvironmentName application path at `"$environmentPath`"" + $identity = "GetHelm_App_$EnvironmentName" + Write-Host "##teamcity[buildProblem description='$errorMessage' identity='$identity']" + Write-Error "$logLead : $errorMessage" + return + } + + $valuesFileChildPath = "$EnvironmentName/values.$EnvironmentName.yaml" + $valuesFileParentPath = Join-Path -Path $RepoPath -ChildPath $ChildPath + $valuesFilePath = Join-Path -Path $valuesFileParentPath -ChildPath $valuesFileChildPath + + if (-not (Test-Path $valuesFilePath)) { + + # While we don't normally do TC messages in Module files, this seems appropriate per conversations and use-case + $errorMessage = "Could not locate values file configured for environment $EnvironmentName at `"$valuesFilePath`"" + $identity = "GetHelm_Values_$EnvironmentName" + Write-Host "##teamcity[buildProblem description='$errorMessage' identity='$identity']" + Write-Error "$logLead : $errorMessage" + return + } + + # Load the values file into an ordered dictionary so that services will be written out in the same order that they were read in. + $valuesRaw = Get-Content -Path $valuesFilePath -Raw + $valuesYaml = ConvertFrom-Yaml -Yaml $valuesRaw -Ordered + + return @{ + values = $valuesYaml + valuesPath = $valuesFilePath + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.tests.ps1 new file mode 100644 index 0000000..5e42217 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmApplicationYamls.tests.ps1 @@ -0,0 +1,40 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-HelmApplicationYamls" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "SUT" } + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith {} + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith {} + # Don't reimplement core module functionality, everything should be driven from inputs + # Mock -ModuleName $moduleForMock -CommandName Join-Path -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Context "It does not error on the happy path" { + It "Does not throw" { + { Get-HelmApplicationYamls -RepoPath "TestDrive:\" -EnvironmentName "pester" } | Should Not Throw + } + It "Does not return an empty item" { + Get-HelmApplicationYamls -RepoPath "TestDrive:\" -EnvironmentName "pester" | Should Not BeNullOrEmpty + } + } + + # Tests as documentation, these are the expected throwing conditions for sure + Context "Getting bad file data throws as expected" { + It "Throws for no content" { + Mock -ModuleName $moduleForMock -CommandName ConvertFrom-Yaml -MockWith { throw "No content was processed"} + { Get-HelmApplicationYamls -RepoPath "TestDrive:\" -EnvironmentName "pester" } | Should Throw + } + It "Throws for not being able to find the file" { + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith { throw "Can't find the file" } + { Get-HelmApplicationYamls -RepoPath "TestDrive:\" -EnvironmentName "pester" } | Should Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.ps1 new file mode 100644 index 0000000..993d3b7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.ps1 @@ -0,0 +1,67 @@ +function Get-HelmDeploymentMetadata { + <# +.SYNOPSIS + Gets the metadata for the services to be deployed defined by Helm values files for the given environment. + Disclaimer: For now this only loads the BASE environment type values and not the environment-specific yamls. + +.PARAMETER RepoPath + The path to the gitops repository containing the helm environment definitions. + +.PARAMETER ReturnAllData + Instead of just returning keys with alk-svc-* returns all keys + +.EXAMPLE + # This gets the list of services and their configurations from the base env type helm values file. + $services = Get-HelmDeploymentMetadata -RepoPath "./Alkami.Gitops.Kubernetes" + + # Each service has each of the properties defined in the values file as a dictionary. + $services[0].serviceName + $services[1].newrelic.appName +#> + [CmdletBinding()] + [OutputType([Object[]])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath, + [Parameter(Mandatory = $false)] + [bool]$ReturnAllData = $false + ) + + $logLead = Get-LogLeadName + + # Construct the path to the directory for the environment base values file. + $svcsDir = Join-Path $RepoPath "alkami-services" + if (!(Test-Path $svcsDir)) { + Write-Error "$logLead : Cannot locate alkami-services directory for $RepoPath" + return + } + + $valuesFilePath = Join-Path $svcsDir "values.yaml" + if (!(Test-Path $valuesFilePath)) { + Write-Error "$logLead : Could not locate values file for environment at $valuesFilePath" + return + } + + # Now that we know what the values file is, read service definitions out of it. + $values = (Get-Content -Path $valuesFilePath -Raw) | ConvertFrom-Yaml -Ordered + if ($null -eq $values) { + Write-Error "$logLead : Could not load configured values file $valuesFilePath" + return + } + + # Convert all of the "flat" yaml properties for the services into a list of services and their properties. + $properties = @() + foreach ($key in $values.Keys) { + # Identify which are services (as opposed to other global configurations) by which sections have "alk-svc-*" in the name. + if ($returnAllData -eq $false) { + if ($key -like "alk-svc-*" ) { + $properties += $values[$key] + } + } else { + $properties += $values[$key] + } + } + + return $properties +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.tests.ps1 new file mode 100644 index 0000000..5909b8e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-HelmDeploymentMetadata.tests.ps1 @@ -0,0 +1,91 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force + +$moduleForMock = "" + +Describe "Get-HelmDeploymentMetadata" { + + # Mock Helm Yaml values file with some globals and three services defined. + $global:testHelmValuesFileContent = @' +# Globals +global: + someBoolean: true + +# Config Map +configMap: + environmentType: Development + +alk-svc-rpc-iaccountservicecontract-v2: + serviceName: "alk-svc-rpc-iaccountservicecontract-v2" + image: alkami.microservices.iaccountservicecontract.netcore + tag: 1.0.0 + +alk-svc-rpc-isettingsservicecontract-v4: + serviceName: "alk-svc-rpc-isettingsservicecontract-v4" + image: alkami.microservices.isettingsservicecontract.netcore + tag: 2.0.0 + +alk-svc-rpc-itransactionservicecontract-v1: + serviceName: "alk-svc-rpc-isettingsservicecontract-v1" + image: alkami.microservices.itransactionservicecontract.netcore + tag: 3.0.0 + firstVariable: + secondVariable: testValue +'@ + + Context "Get-HelmDeploymentMetadata Contains Expected Data" { + + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith { return $testHelmValuesFileContent } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + $result = Get-HelmDeploymentMetadata -RepoPath "C:/does/not/matter/its/mocked" + + It "should contain all expected services" { + $result.count | Should -Be 3 + $result | Where-Object { $_.serviceName -eq "alk-svc-rpc-iaccountservicecontract-v2" } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.serviceName -eq "alk-svc-rpc-isettingsservicecontract-v4" } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.serviceName -eq "alk-svc-rpc-isettingsservicecontract-v1" } | Should -Not -BeNullOrEmpty + } + + It "should not contain globals or configmaps" { + $result | Where-Object { $_.someBoolean -eq $true } | Should -BeNull + $result | Where-Object { $_.environmentType -eq "Development" } | Should -BeNull + } + + It "contains arbitrary multi-level data" { + $settingsService = $result | Where-Object { $_.serviceName -eq "alk-svc-rpc-isettingsservicecontract-v1" } + $settingsService | Should -Not -BeNullOrEmpty + $settingsService.firstVariable.secondVariable | Should -Be "testValue" + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 0 -Scope Context + } + } + + Context "Get-HelmDeploymentMetadata Expected Failures" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + It "should fail if the repository path is bad" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -ParameterFilter { $Path -eq "./BadRepoPath" } -MockWith { return $false } + + $result = Get-HelmDeploymentMetadata -RepoPath "./BadRepoPath" + $result | Should -BeNullOrEmpty + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + + It "should fail if the values file is missing" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -ParameterFilter { $Path -eq "./GoodRepositoryPath" } -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-Path -ParameterFilter { $Path -eq "./GoodRepositoryPath/alkami-services/values.yaml" } -MockWith { return $false } + + $result = Get-HelmDeploymentMetadata -RepoPath "./GoodRepositoryPath" + $result | Should -BeNullOrEmpty + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Times 1 -Scope Context + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-LogPathsForOrbApplication.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-LogPathsForOrbApplication.ps1 new file mode 100644 index 0000000..d3fc1ec --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-LogPathsForOrbApplication.ps1 @@ -0,0 +1,26 @@ +Function Get-LogPathsForOrbApplication { + <# +.SYNOPSIS + Gets the paths where a given ORB application saves its log files + +.PARAMETER AppName + Name of the ORB Application to get the log file paths for +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("Name")] + [string]$AppName + ) + process { + $log4netConfigPath = (Join-Path (Join-Path (Get-OrbPath) $appName) "log4net.config"); + if (!(Test-Path $log4netConfigPath)) { + throw "Could not find the path at $log4netConfigPath"; + } + + $config = [Xml](Get-Content $log4netConfigPath); + + ## Use the native powershell capability to just do the array to list thing automatically + return $config.configuration.log4net.appender.file.value; + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-MachineConfigServiceAccount.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-MachineConfigServiceAccount.ps1 new file mode 100644 index 0000000..e5dca22 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-MachineConfigServiceAccount.ps1 @@ -0,0 +1,44 @@ +Function Get-MachineConfigServiceAccount { +<# +.SYNOPSIS + Retrieves either a database-access or a non-database-access account to run as for a given service account. + +.DESCRIPTION + Retrieves either a database-access or a non-database-access account to run as for a given service account. + This is based on the value in the machine.config for either NonDatabaseMicroServiceAccount or DatabaseMicroServiceAccount + If this value has not been configured or is null this method will throw an error + +.PARAMETER IsDatabaseAccessRequired + [switch] Does this user require database access? + +.PARAMETER ComputerName + [string] The remote computer to connect to. Defaults to localhost. + +.OUTPUTS + Returns a config setting for the right service account value to use +#> + [CmdletBinding()] + param ( + [Alias("UseDBUser")] + [switch]$IsDatabaseAccessRequired, + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost" + ) + + $logLead = (Get-LogLeadName) + + $appKey = "NonDatabaseMicroServiceAccount" + + if ($IsDatabaseAccessRequired) { + $appKey = "DatabaseMicroServiceAccount" + } + + $result = (Get-AppSetting -Key $appKey -ComputerName $ComputerName) + + if ([string]::IsNullOrWhiteSpace($result)) { + throw "$logLead : App setting not configured for [$appKey]" + } + + return $result +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.Tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.Tests.ps1 new file mode 100644 index 0000000..5fc0ae9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.Tests.ps1 @@ -0,0 +1,22 @@ +<# +A note for the maintainer in the future + +If this test breaks, it's because the output from Microsoft changed. +We want the parsing to do the right thing. +#> +Describe "Ensure netsh commands match expected output" { + $output = (netsh int ipv4 show excludedportrange protocol=tcp) + + $expectedValues = @( + 'Protocol tcp Port Exclusion Ranges', + '\* - Administered port exclusions.', # use of \* as we use a -match operator and this is therefore a regex + 'Start Port', + 'End Port' + ) + + foreach ($expectedValue in $expectedValues) { + It "Checking for matching case on [$expectedValue] to be present" { + $output -match $expectedValue | Should -Be $true + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.ps1 new file mode 100644 index 0000000..2950111 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshExcludedPortRanges.ps1 @@ -0,0 +1,28 @@ +function Get-NetshExcludedPortRanges { +<# +.SYNOPSIS + Get the existing port ranges and parse them into actionable items +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + ) + + $ranges = @() + + $existingExcludedPortRanges = (netsh int ipv4 show excludedportrange protocol=tcp) + + foreach($existingRange in $existingExcludedPortRanges) { + $cleanedRange = $existingRange.Replace('Protocol tcp Port Exclusion Ranges','').Replace('* - Administered port exclusions.','').Replace('Start Port','').Replace('End Port','').Replace('--','').Trim() + if ([string]::IsNullOrWhiteSpace($cleanedRange)) { + continue + } + $splits = $cleanedRange -split '\s+' + if ($splits.Length -gt 1) { + $rangeStart = [int]$splits[0] + $rangeEnd = [int]$splits[1] + $ranges += @{ Start = $rangeStart; End = $rangeEnd;} + } + } + return $ranges +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.Tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.Tests.ps1 new file mode 100644 index 0000000..5495a5e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.Tests.ps1 @@ -0,0 +1,20 @@ +<# +A note for the maintainer in the future + +If this test breaks, it's because the output from Microsoft changed. +We want the parsing to do the right thing. +#> +Describe "Ensure netsh commands match expected output" { + $output = (netsh http show iplisten) + + $expectedValues = @( + 'IP addresses present in the IP listen list:', + '-------------------------------------------' + ) + + foreach ($expectedValue in $expectedValues) { + It "Checking for matching case on [$expectedValue] to be present" { + $output -match $expectedValue | Should -Be $true + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.ps1 new file mode 100644 index 0000000..69cd6f2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NetshHttpIPListens.ps1 @@ -0,0 +1,25 @@ +function Get-NetshHttpIPListens { +<# +.SYNOPSIS + Get the existing HttpIPListens and parse them into actionable items +#> +# Unit testing note: This is not unit tested because it relies on the barebones system call implementation on netsh + [CmdletBinding()] + [OutputType([string[]])] + param( + ) + + $ipListens = @() + + $existingIPListens = (netsh http show iplisten) + + foreach($listen in $existingIPListens) { + $cleanedListen = $listen.Replace('IP addresses present in the IP listen list:','').Replace('-------------------------------------------','').Trim() + if ([string]::IsNullOrWhiteSpace($cleanedListen)) { + continue + } + $ipListens += $cleanedListen.Trim() + } + + return $ipListens +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.ps1 new file mode 100644 index 0000000..853ff67 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.ps1 @@ -0,0 +1,57 @@ +Function Get-NewRelicAppNameForConfigurationValue { +<# +.SYNOPSIS + Get the App Name for setting the config file value for New Relic + +.DESCRIPTION + Legacy ORB components + +.PARAMETER ServiceName + [string] This is the default name of the service as expected for the service +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) + + $logLead = (Get-LogLeadName); + + # The use of the package names in this list is due to the fact that these services/apps were extracted to packages from legacy + $NewRelicNames = @{ + AuditService = "Audit"; + BankService = "Bank"; + ContentService = "Content"; + CUFX = "CUFX"; + "Alkami.Api.CUFX" = "CUFX"; + ExceptionService = "Exception"; + IPSTS = "IPSTS"; + CoreService = "Core"; + MessageCenterService = "MessageCenter"; + NagConfigurationService = "NagConfiguration"; + NotificationService = "Notification"; + ORBFX = "ORBFX"; + "Alkami.Api.OrbFX" = "ORBFX"; + "RP-STS" = "RP-STS"; + "Alkami.Security.RPSTS" = "RP-STS"; + SchedulerService = "Scheduler"; + SecurityManagementService = "SecurityManagement"; + STSConfiguration = "STSConfiguration"; + SymConnectMultiplexer = "SymConnect"; + "Alkami.App.Providers.SymConnectMultiplexer" = "SymConnect"; + WebClient = "WebClient"; + WebClientAdmin = "Admin"; + Radium = "Radium Service"; + Nag = "Nag"; + TextBanking = "TextBanking" + } + + $NewRelicName = $NewRelicNames[$ServiceName] + + if (![string]::IsNullOrWhiteSpace($NewRelicName)) { + Write-Verbose "$logLead : Found a dereference value of $NewRelicName for $ServiceName" + return $NewRelicName + } + + return $ServiceName +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.tests.ps1 new file mode 100644 index 0000000..759ddbf --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicAppNameForConfigurationValue.tests.ps1 @@ -0,0 +1,44 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NewRelicAppNameForConfigurationValue" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Context "Testing basic matrix values" { + + It "Testing known matrix value" { + Get-NewRelicAppNameForConfigurationValue "BankService" | Should Be "Bank" + } + It "Testing unknown matrix value" { + Get-NewRelicAppNameForConfigurationValue "BankService2" | Should Be "BankService2" + } + } + + Context "Testing basic microservice values" { + + It "Testing known matrix value" { + Get-NewRelicAppNameForConfigurationValue "Cole.MS.Magic.Host" | Should Be "Cole.MS.Magic.Host" + } + It "Testing unknown matrix value" { + Get-NewRelicAppNameForConfigurationValue "Alkami.MS.Magic.Host" | Should Be "Alkami.MS.Magic.Host" + } + } + + Context "Testing CUFX package values" { + + It "Testing known matrix value" { + Get-NewRelicAppNameForConfigurationValue "CUFX" | Should Be "CUFX" + } + It "Testing known matrix value" { + Get-NewRelicAppNameForConfigurationValue "Alkami.Api.CUFX" | Should Be "CUFX" + } + } +} + +#endregion Get-ConfigurationFiles \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.ps1 new file mode 100644 index 0000000..8e00465 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.ps1 @@ -0,0 +1,69 @@ +function Get-NewRelicNextLink { +<# +.SYNOPSIS + Takes a Link header value (in the case of an invalidly formatted header, $null, or "", will return "") + +.DESCRIPTION + Assumption (per docs) you will get a header that looks like this: + + Link: ;rel="first", +;rel="prev", +;rel="next", +;rel="last" + + Find the "next" header line, get the url from it. + + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link and https://tools.ietf.org/html/rfc5988 + +.PARAMETER LinkHeader + The header key to parse + +.PARAMETER LinkType + The link type to look for. Values: first,prev,next,last + +.EXAMPLE + Get-NewRelicNextLink -LinkHeader Link: ';rel="first", ;rel="prev", ;rel="next", ;rel="last"' + +"https://api.newrelic.com/v2/applications/5313884/metrics.xml?page=20" +.EXAMPLE + Get-NewRelicNextLink -LinkHeader Link: ';rel="first", ;rel="prev", ;rel="next", ;rel="last"' -LinkType Last + +"https://api.newrelic.com/v2/applications/5313884/metrics.xml?page=20" +#> + [CmdletBinding()] + param ( + [string]$LinkHeader, + [ValidateSet("first","prev","next","last")] + [string]$LinkType = "next" + ) + + # It's ok to just not pass in a header value + if ([string]::IsNullOrWhiteSpace($LinkHeader)) { + return "" + } + + $links += @($LinkHeader.Split(",")) + + # This shape will look like @{ first = url; next = url; last = url; ... } + $linkHash = @{} + + if ($links.Count -gt 0) { + foreach($link in $links) { + # This is idiosyncratic powershell and does magic where it basically splits on either value + # You can't combine them, but you could write some complex loop logic to replace it. + # Code we don't write is code we don't have to support. + $linkSplits = $link -split '; rel=' -split ';rel=' + if (@($linkSplits).Count -gt 0) { + # lop off the first and last chars (<,>,") of each string + $url = $linkSplits[0].Trim().Substring(1,$linkSplits[0].Trim().Length - 2) + $name = $linkSplits[1].Trim().Substring(1,$linkSplits[1].Trim().Length - 2) + + # stuff them into an object + $linkHash[$name] = $url + Write-Verbose "$logLead : Found a url for '$name' - '$url'" + } + } + } + + return $linkHash[$LinkType] +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.tests.ps1 new file mode 100644 index 0000000..aa0aace --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicNextLink.tests.ps1 @@ -0,0 +1,41 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NewRelicNextLink" { + Context "Has garbage input" { + It "Throws on garbage input" { + { (Get-NewRelicNextLink -LinkHeader "garbage input") } | Should -Throw + } + } + + Context "Has empty input" { + $isEmpty = (Get-NewRelicNextLink -LinkHeader "") + + It "is empty" { + $isEmpty | Should -BeNull + } + } + + Context "Has correct input" { + # this test has no spaces between ; and rel - This is test case #1 for parsing the values + $isRight = (Get-NewRelicNextLink -LinkHeader ';rel="first", ;rel="prev", ;rel="next", ;rel="last"') + + It "is the expected value" { + $isRight | Should -Be "https://api.newrelic.com/v2/applications/5313884/metrics.xml?page=20" + } + } + + Context "Has correct input but the expected type is not present" { + # this test has one space between ; and rel - This is test case #2 for parsing the values + $isRight = (Get-NewRelicNextLink -LinkHeader '; rel="first", ; rel="prev", ; rel="next"' -LinkType 'last') + + It "is empty" { + $isEmpty | Should -BeNull + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.ps1 new file mode 100644 index 0000000..2571716 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.ps1 @@ -0,0 +1,128 @@ +function Get-NewRelicObjects { +<# +.SYNOPSIS + Given a url, find the objects and continue to follow the next-link chain if present in the URL headers from NewRelic + +.DESCRIPTION + Given a url, find the objects and continue to follow the next-link chain if present in the URL headers from NewRelic. + Assumption: Link returns an object of content @{ : } that also returns a header object of "Link: " + For more detail on links, see Get-NewRelicNextLink + +.PARAMETER ApiKey + The NR supplied API key + +.PARAMETER ApiKeyName + Sometimes NR wants unusual key names. The default is X-Api-Key + +.PARAMETER InitialUrl + This url can reach out to NR + +.PARAMETER ObjectKey + What the response object comes back as. Normally matches the last element in the initial url name (except any .json/.xml suffix) + +.PARAMETER FilterKey + Used to help provide a filter to the API request + +.PARAMETER FilterValue + Used to help provide a filter to the API request + +.EXAMPLE + Get-NewRelicObjects -ApiKey -InitialUrl "https://synthetics.newrelic.com/synthetics/api/v3/monitors" -ObjectKey "monitors" + +.EXAMPLE + Get-NewRelicObjects -ApiKey -InitialUrl "https://synthetics.newrelic.com/synthetics/api/v3/monitors" + +.EXAMPLE + $applications = Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" + + $applications is a simple object with property applications that contains the data + +.EXAMPLE + $applications = Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications" + + $applications is a simple object with property applications that contains the data + +.EXAMPLE + $applications = (Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications" -FilterKey "name" -FilterValue "Some.Package.Id").applications + + $applications is an array of application objects +#> + [CmdletBinding()] + [OutputType([System.Object])] + param ( + [Parameter(Mandatory = $true)] + [string]$ApiKey, + [Parameter(Mandatory = $true)] + [string]$InitialUrl, + [string]$ObjectKey, + [string]$ApiKeyName = "X-Api-Key", + [string]$FilterKey, + [string]$FilterValue + ) + + $logLead = (Get-LogLeadName) + + if([string]::IsNullOrWhiteSpace($ObjectKey)) { + $urlEnd = ($initialUrl -split '/')[-1] + $urlEndSplits = $urlEnd.Split('.') + $ObjectKey = $urlEndSplits[0] + } + + $headers = @{ $ApiKeyName = $ApiKey; "Content-Type" = "application/json";} + $method = "GET" + + $filterResults = $false + $filterKeyProvided = ![string]::IsNullOrWhiteSpace($FilterKey) + $filterValueProvided = ![string]::IsNullOrWhiteSpace($FilterValue) + if ($filterKeyProvided -and !$filterValueProvided) { + Write-Warning "$logLead : FilterKey provided with no FilterValue - No filter being used" + } elseif (!$filterKeyProvided -and $filterValueProvided) { + Write-Warning "$logLead : FilterKey provided with no FilterValue - No filter being used" + } elseif ($filterKeyProvided -and $filterValueProvided) { + $separator = "?" + if ($InitialUrl.IndexOf('?') -gt -1) { + $separator = "&" + } + $query = "$($separator)filter[$FilterKey]=$FilterValue" + $filterResults = $true + } + + $nextUrl = "$InitialUrl$query" + $lastUsedUrl = $nextUrl + + $content = @() + + try { + while (![string]::IsNullOrWhiteSpace($nextUrl)) { + $response = (Invoke-WebRequest -Uri $nextUrl -UseBasicParsing -Headers $headers -Method $method) + + $newContent = @(($response.Content | ConvertFrom-Json).$ObjectKey) + + if ($filterResults) { + $newContent = $newContent.Where({$_ -match "$FilterValue "}) + } + + $content += $newContent + + $lastUsedUrl = $nextUrl + $nextUrl = (Get-NewRelicNextLink $response.Headers.Link) + + if ($lastUsedUrl -eq $nextUrl) { + Write-Verbose "$logLead : Returned URL matches previously used url, this indicates we are in a loop. Not fetching further data" + $nextUrl = "" + } + Write-Host $nextUrl + } + } catch [System.Net.WebException] { + $errorMessage = $_.Exception.Message + $contentResponse = (New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() + Write-Warning "$logLead : Could not get new relic objects. Error message: [$errorMessage]. Content of response was [$contentResponse]" + throw + } catch { + $errorMessage = $_.Exception.Message + Write-Warning "$logLead : Could not get new relic objects. Error message: [$errorMessage]" + throw + } + + return @{ $ObjectKey = $content; } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.tests.ps1 new file mode 100644 index 0000000..87ca2ee --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-NewRelicObjects.tests.ps1 @@ -0,0 +1,93 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-NewRelicObjects" { + $apiKey = "this key is garbage" + + Mock -ModuleName $moduleForMock Get-LogLeadName -MockWith { return "[UUT]" } + Mock -ModuleName $moduleForMock Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock Get-NewRelicNextLink -MockWith { } + + Context "Gets an empty response from NR if no data returned" { + Mock -ModuleName $moduleForMock Invoke-WebRequest -MockWith { + $response = New-MockObject -Type ([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]) + $content = '{"applications": []}' + $statusCode = 200 + $response | Add-Member -NotePropertyName Content -NotePropertyValue $content -Force + $response | Add-Member -NotePropertyName StatusCode -NotePropertyValue $statusCode -Force + + return $response + } + + It "is empty" { + $applications = (Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications" -FilterKey "name" -FilterValue "Some.Package.Id").applications + $applications | Should -BeNullOrEmpty + } + } + + Context "Gets a response with values from NR if some data returned" { + Mock -ModuleName $moduleForMock Invoke-WebRequest -MockWith { + $response = New-MockObject -Type ([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]) + $content = @" +{"applications": [ + {"id":1234,"name":"Unit Test Environment sample application"}, + {"id":5678,"name":"Unit Test Environment Alkami.Microservice Test 1"}, + {"id":1010,"name":"Unit Test Environment Alkami.Microservice Test 2"}, + {"id":9876,"name":"Fake Pod 12 Alkami.Microservice Test 3"}, + {"id":8765,"name":"Fake Pod 12.1 Alkami.Microservice Test 4"} +]} +"@ + $statusCode = 200 + $response | Add-Member -NotePropertyName Content -NotePropertyValue $content -Force + $response | Add-Member -NotePropertyName StatusCode -NotePropertyValue $statusCode -Force + + return $response + } + + $applications = (Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications").applications + + It "is not empty" { + $applications | Should -Not -BeNullOrEmpty + } + + It "has 5 elements" { + $applications | Should -HaveCount 5 + } + } + + Context "Gets a response with values from NR if some data returned" { + Mock -ModuleName $moduleForMock Invoke-WebRequest -MockWith { + $response = New-MockObject -Type ([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]) + $content = @" +{"applications": [ + {"id":1234,"name":"Unit Test Environment 1 sample application"}, + {"id":5678,"name":"Unit Test Environment 1 Alkami.Microservice Test 1"}, + {"id":1010,"name":"Unit Test Environment 1 Alkami.Microservice Test 2"}, + {"id":1234,"name":"Unit Test Environment 12 sample application"}, + {"id":5678,"name":"Unit Test Environment 12 Alkami.Microservice Test 1"}, + {"id":1010,"name":"Unit Test Environment 12 Alkami.Microservice Test 2"} +]} +"@ + $statusCode = 200 + $response | Add-Member -NotePropertyName Content -NotePropertyValue $content -Force + $response | Add-Member -NotePropertyName StatusCode -NotePropertyValue $statusCode -Force + + return $response + } + + $applications = (Get-NewRelicObjects -apiKey $apiKey -initialUrl "https://api.newrelic.com/v2/applications.json" -ObjectKey "applications" -FilterKey "name" -FilterValue "Unit Test Environment 1").applications + + It "is not empty" { + $applications | Should -Not -BeNullOrEmpty + } + + It "has 3 filtered elements" { + $applications | Should -HaveCount 3 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-OrbSymLinkFolderNames.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-OrbSymLinkFolderNames.ps1 new file mode 100644 index 0000000..5ca55df --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-OrbSymLinkFolderNames.ps1 @@ -0,0 +1,16 @@ +function Get-OrbSymLinkFolderNames { + <# + +.SYNOPSIS + returns a list of orb folders that should be symlinked to orb/shared +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + $linkFolders = @( + "IPSTS" + "WebClientAdmin" + "WebClient" + ) + return $linkFolders +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.ps1 new file mode 100644 index 0000000..f15b12d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.ps1 @@ -0,0 +1,193 @@ +function Get-PackageManifest { +<# +.SYNOPSIS + Given a path, find the AlkamiManifest in that location. + However, this should return either an xml, json, etc formatted value as a dotted object + This also validates the manifest to be valid + +.PARAMETER Path + This can be the path to the known manifest file, or to a folder. + This will find the first manifest preferring xml over json etc, in a given folder. + This folder does not expect to find manifests lower than the current file, but may look in the parent folder for a manifest + +.PARAMETER RawContent + Can be used to pass in raw content to let it be parsed for us + +.PARAMETER SkipTests + Use this to skip tests when reading the package. Use sparingly. + +.PARAMETER PackageName + Optional package name. + +.PARAMETER ManifestSource + Optional information including the feed source, name, and version +#> + [CmdletBinding(DefaultParameterSetName = 'Path')] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + [ValidateNotNullOrEmpty()] + $Path, + [Parameter(Mandatory = $true, ParameterSetName = 'RawContent')] + [ValidateNotNullOrEmpty()] + $RawContent, + [switch]$SkipTests, + [Parameter(Mandatory = $false)] + [string]$PackageName, + [Parameter(Mandatory = $false)] + [string]$ManifestSource + ) + + $logLead = Get-LogLeadName + $fileContent = $RawContent + $extension = $null + + if ($PSCmdlet.ParameterSetName -eq 'Path') { + $isPackage = $false + $packagePath = (Get-ChocolateyInstallPath) + + $validManifestFilenames = (Get-ValidPackageManifestFilenames) + + if (!(Test-Path $Path)) { + throw "$logLead : Provided path was not valid, or is not found. Please ensure your value is a valid path on disk. Path: [$Path]" + } + + # Normally this would write an error to indicate a problem + # We don't need that, we're just curious if it matches a package location so we can treat it like a package + if (Test-PathIsInApprovedPackageLocation -Path $Path -ErrorAction SilentlyContinue) { + $isPackage = $true + } + + $item = (Get-Item $Path) + $leafName = $null + if (!$item.PSIsContainer) { + # path was a folder + $leafName = (Split-Path -Path $Path -Leaf) + # make $Path be a folder only + $Path = (Split-Path -Path $Path -Parent) + } else { + # Otherwise $Path is already a folder + } + + if (![string]::IsNullOrWhiteSpace($leafName)) { + if (!$validManifestFilenames.Contains($leafName)) { + Write-Warning "$logLead : Supplied file is not a valid manifest name. Defaulting to a manifest filename lookup" + $leafName = $null + } + } + + if ([string]::IsNullOrWhiteSpace($leafName)) { + # this means "label the loop that you want to exit early in the break statement later" + :fileLookup do { + foreach ($candidateName in $validManifestFilenames) { + $candidatePath = (Join-Path -Path $Path -ChildPath $candidateName) + Write-Verbose "$logLead : Looking for a manifest candidate at [$candidatePath]" + $item = (Get-Item -Path $candidatePath -ErrorAction Ignore) + if ($null -ne $item) { + # we found an item, this is good + $leafName = $candidateName + break fileLookup + } + } + + $Path = (Split-Path $Path -Parent) + if ([string]::IsNullOrWhiteSpace($Path)) { + Write-Error "Could not find an AlkamiManifest in the specified folder or parent folders" + return $null + } + + if ($isPackage -and (Test-PathsAreEqual -Source $Path -Target $packagePath)) { + Write-Error "$logLead : Parent package folder found, no manifest found. Please verify expected folder contains a manifest." + return $null + } + } while ($true) + } + + $manifestPath = (Join-Path -Path $Path -ChildPath $leafName) + + if (!(Test-Path $manifestPath)) { + Write-Error "$logLead : Could not find manifest file in path [$Path]" + } + + $fileContent = (Get-Content -Path $manifestPath -Raw) + + if ([string]::IsNullOrWhiteSpace($fileContent)) { + throw "$logLead : Content of [$manifestPath] is empty or whitespace. This file can not be empty for validation to continue" + } + + $extension = [System.IO.Path]::GetExtension($manifestPath) + } else { + # fileContent was set to rawContent + if ($null -ne $fileContent) { + if ($fileContent -is [string]) { + $firstCharacter = $fileContent[0] + if ($firstCharacter -eq '<') { + $extension = '.xml' + } + if ($firstCharacter -eq '{') { + $extension = '.json' + } + } elseif ($fileContent -is [xml]) { + $extension = '.xml' + } else { + # heck, idk, let's pretend it's json and try it and see what happens + $extension = '.json' + } + if (!(Test-StringIsNullOrWhitespace -Value $ManifestSource)) { + $manifestPath = $ManifestSource + } elseif (!(Test-StringIsNullOrWhitespace -Value $PackageName)) { + $manifestPath = $PackageName + } else { + $manifestPath = "File content was provided with no name" + } + } + } + + $targetObject = $null + + switch ($extension) { + '.json' { $targetObject = (ConvertFrom-Json $fileContent) } + '.xml' { + try { + $targetObject = (ConvertFrom-Xml ([xml]($fileContent))).packageManifest + } catch [System.Management.Automation.RuntimeException] { + if ($null -ne $_.FullyQualifiedErrorId -and $_.FullyQualifiedErrorId -eq "InvalidCastToXmlDocument") { + if($null -eq $PackageName) + { + # Append a random 4 digit number so we don't get identity collisions + $date = Get-Date + $random = Get-Random -Maximum 10000 -SetSeed $date.Millisecond + $PackageName = "UnknownPackageName_$random" + } + + $problemMessage = "Couldn't parse manifest XML for [$PackageName]. Please contact the appropriate dev team." + Write-Warning "$loglead : $problemMessage. Exception was:" + Write-Warning $_.Exception + + $sanitizedProblemMessage = ConvertTo-SafeTeamCityMessage -InputText $problemMessage + + Write-Host "##teamcity[buildProblem description='$sanitizedProblemMessage' identity='$PackageName']" + } + } + # even if there was a content there, it may not have had a root called + # anchoring to remove the .packageManifest reduces a lot of other complexities elsewhere + if ($null -eq $targetObject) { + throw "$logLead : No valid packageManifest found in [$manifestPath]" + } + } + default {throw "$logLead : The content passed in appears to not be xml or json. Please verify the source data is an AlkamiManifest content."} + } + + # Can't combine the two jumps because it doesn't early-abort on comparisons + # Use skipping tests sparingly, prefer to only when they are already installed. + # That does not mean "comparing the path is in the package repository location" + # because for chocolatey it always will be, it's small sub-paths differentiation there. + if (!$SkipTests) { + if (!(Test-AlkamiManifest -TargetObject $targetObject)) { + Write-Warning "$logLead : Manifest at [$manifestPath] failed processing. Please resolve errors and reprocess." + throw "$logLead : Can not parse manifest properly." + } + } + + return $targetObject +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.tests.ps1 new file mode 100644 index 0000000..ced1912 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-PackageManifest.tests.ps1 @@ -0,0 +1,55 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-PackageManifest" { + + Context "Assembly and Package is provided in Manifest" { + + Mock -CommandName Write-Warning -MockWith {} + Mock -CommandName Test-AlkamiManifest -MockWith { $false } + + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.FluxManagement.Service.Host + Service + + + framework + Alkami.MicroServices.FluxManagement.Service.Host + + + Alkami.MicroServices.FluxManagement.Migrations.dll + + + FluxManagement_Service + + +"@ + + $FeedSource = "FakeFeed" + $Name = "FakeApp" + $Version = "1.23" + + It "Should Throw" { + + { Get-PackageManifest -RawContent $sampleManifest -PackageName $Name -ManifestSource "$FeedSource $Name $Version" } | Should Throw + } + + It "Manifest warning equals fake values" { + + { Get-PackageManifest -RawContent $sampleManifest -PackageName $Name -ManifestSource "$FeedSource $Name $Version" } | Should Throw + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -eq + "[Get-PackageManifest] : Manifest at [FakeFeed FakeApp 1.23] failed processing. Please resolve errors and reprocess." } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-RedisConnectionString.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-RedisConnectionString.ps1 new file mode 100644 index 0000000..855f958 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-RedisConnectionString.ps1 @@ -0,0 +1,47 @@ +function Get-RedisConnectionString { +<# +.SYNOPSIS + Returns the Redis Connection String +.DESCRIPTION + Reads the value from the appSetting named "RedisSetting" from the machine.config ConnectionStrings block. Failing that it checks for an appSetting. If a client connection is provided, + it lastly checks the database. +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory = $false)] + [PSObject]$Client + ) + + $machineConfig = Read-MachineConfig + + # Get from ConnectionStrings + $redisConnectionString = ($machineConfig.Configuration.ConnectionStrings.ChildNodes | Where-Object {$_.Name -eq "RedisSetting"}) + if ($null -ne $redisConnectionString) { + return ($redisConnectionString | Select-Object -ExpandProperty connectionString) + } + + # Get from AppSettings + $redisAppSetting = Get-AppSetting "RedisHostCommaSeperatedEndpointsWithPorts" + if ($null -ne $redisAppSetting) { + return $redisAppSetting + } + + if ($null -ne $Client) { + # Get from Database + $queryString = "select s.Value + from core.provider p + join core.item i on i.parentid = p.id + left join core.ItemSetting s on s.ItemId = i.Id + join core.ProviderType pt on pt.ID = p.ProviderTypeID + where p.Name like 'RedisCacheProvider' + and i.ItemType = 'CacheProvider' + and s.Name = 'HostCommaSeperatedEndpointsWithPorts' + order by p.Name" + + return (Invoke-QueryOnClientDatabase $Client $queryString) + } + + return $null +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-RegistryKeyValue.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-RegistryKeyValue.ps1 new file mode 100644 index 0000000..3dfc6c6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-RegistryKeyValue.ps1 @@ -0,0 +1,35 @@ +function Get-RegistryKeyValue { +<# +.SYNOPSIS +Get a registry key's value + +.DESCRIPTION +Can be used to get a registry key's value for a given Path. Uses Split-Path leaf to as registry key's value. + +.EXAMPLE +Get-RegistryKeyValue -RegKey HKCU:\Environment\foo\bar -Verbose +#> + + [CmdletBinding()] + param ( + $RegKey + ) + $regKeyData = $null + $logLead = Get-LogLeadName + $regKeyName = Split-Path $RegKey -Parent + $regKeyValue = Split-Path $RegKey -Leaf + + try { + if (Test-RegistryKey $regKeyName) { + $regKeyData = Get-ItemPropertyValue $regKeyName -Name $regKeyValue + } else { + throw "$logLead : Registry Key ($regKeyName) not found" + } + } catch [System.Management.Automation.ItemNotFoundException] { + Write-Error "$logLead : $regKeyName does not exist" + } catch { + Write-Error "$logLead : $_" + } + + return $regKeyData +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-RemoteDotNetConfigPath.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-RemoteDotNetConfigPath.ps1 new file mode 100644 index 0000000..d1429d8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-RemoteDotNetConfigPath.ps1 @@ -0,0 +1,21 @@ +function Get-RemoteDotNetConfigPath { +<# +.SYNOPSIS + Returns the path to a remote 64-bit machine.config +.NOTES + Will not work in its current form for load balanced pods/lanes +#> + [CmdletBinding()] + [OutputType([string])] + param( + ) + + if (!(Test-IsWebServer)) { + Write-Warning "This function can only be called from a web server." + return $null + } + + $bankServiceIP = Resolve-DnsName -Name "bankservice" -Type A + + ("\\{0}\C$\Windows\Microsoft.Net\Framework64\v4.0.30319\Config\machine.config" -f $bankServiceIP.IpAddress) +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ReportServerConfiguration.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ReportServerConfiguration.ps1 new file mode 100644 index 0000000..d5eb002 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ReportServerConfiguration.ps1 @@ -0,0 +1,37 @@ +function Get-ReportServerConfiguration { +<# +.SYNOPSIS + Returns the Report Server Configuration XML block +#> + [CmdletBinding()] + [OutputType([Object])] + param( + ) + + $logLead = (Get-LogLeadName); + + [xml]$machineConfig = Get-Content (Get-DotNetConfigPath) + + if (($null -eq $machineConfig.Configuration.AppSettings) -or + (($machineConfig.Configuration.AppSettings.ChildNodes | Where-Object {$_.Key -match "ReportServer"}).Count -eq 0)) { + Write-Warning "$logLead : Could not find report server configuration in the local machine.config" + return $null + } + + $reportKeys = @("ReportServer", "ReportServerPath", "ReportServerUserName", "ReportServerPassword", "ReportUserName", "ReportPassword", "Environment.Type") + + $reportServerConfiguration = New-Object System.XML.XMLDocument + $root = $reportServerConfiguration.CreateElement("appSettings") + + $appSettingsNode = $machineConfig.Configuration.AppSettings + + foreach ($node in $appSettingsNode.ChildNodes | Where-Object {$reportKeys -contains $_.Key}) { + $newNode = $reportServerConfiguration.ImportNode($node, $false) + $root.AppendChild($newNode) | Out-Null + } + + $reportServerConfiguration.AppendChild($root) | Out-Null + + return $reportServerConfiguration +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ServerRoleEnvironmentalVariable.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ServerRoleEnvironmentalVariable.ps1 new file mode 100644 index 0000000..0901b10 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ServerRoleEnvironmentalVariable.ps1 @@ -0,0 +1,14 @@ +function Get-ServerRoleEnvironmentalVariable { +<# +.SYNOPSIS + Gets the server role environmental variable + Expected values are Web, Microservice, App, Fabric +#> + [CmdletBinding()] + [OutputType([bool])] + param( + ) + + return (Get-EnvironmentVariable -Name "ServerRole" -StoreName Machine) +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageDatabaseConfigFilenames.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageDatabaseConfigFilenames.ps1 new file mode 100644 index 0000000..2872594 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageDatabaseConfigFilenames.ps1 @@ -0,0 +1,15 @@ +function Get-ValidPackageDatabaseConfigFilenames { + <# +.SYNOPSIS + Get the list of valid database config file names + The idea is that if we add more support for additional names, we can add them to this list for iteration. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + ) + + return @( + 'DatabaseConfig.ps1' + ) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageManifestFilenames.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageManifestFilenames.ps1 new file mode 100644 index 0000000..b43a5c1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidPackageManifestFilenames.ps1 @@ -0,0 +1,18 @@ +function Get-ValidPackageManifestFilenames { +<# +.SYNOPSIS + Get the list of valid manifest file names + The idea is that if we add more support (see Get-PackageManifest) for additional types, we can add them to this list for iteration. + Ideally we could make that function lookup-aware on how to get packages, but that would require a synchronized credential for feeds that require credentialized access. + We can accomplish this with a future pattern, but for now, we default to the choco libs +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + ) + + return @( + 'AlkamiManifest.xml' + 'AlkamiManifest.json' + ) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidWebTierInstallLocations.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidWebTierInstallLocations.ps1 new file mode 100644 index 0000000..3327adc --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Get-ValidWebTierInstallLocations.ps1 @@ -0,0 +1,10 @@ +function Get-ValidWebTierInstallLocations { +<# +.SYNOPSIS + Returns the places on web tiers where components may be installed +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + return @('Client', 'Admin', 'Generic') +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.ps1 new file mode 100644 index 0000000..3da7924 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.ps1 @@ -0,0 +1,390 @@ +function New-AlkamiManifest { + <# +.SYNOPSIS + Create a new AlkamiManifest.xml (v 1.0) in a specified location for a known type. + +.PARAMETER Type + The type of manifest to be created + +.PARAMETER Destination + The target folder location where the manifest is going to be created. Defaults to the current folder. + +.PARAMETER ProjectLocation + The location of the project file to use for manifest creation. Defaults to looking in the current folder. + +.PARAMETER FileType + Create files in xml or json format + +.PARAMETER Force + Used to overwrite existing files +#> + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet('Widget', 'Service', 'Migration', 'Theme', 'WebApplication', 'WebExtension', 'Repository', 'Provider', 'WebSite', 'LegacyUtility', 'Hotfix', 'ApiComponent')] + [String]$Type, + + [Parameter(Position = 1, Mandatory = $false)] + [Alias('Path', 'Folder', 'Target')] + [String]$Destination, + + [Parameter(Position = 2, Mandatory = $false)] + [String]$ProjectLocation, + + [Parameter(Position = 3, Mandatory = $false)] + [ValidateSet('Xml', 'Json')] + [string]$FileType = 'Xml', + + [Switch]$Force + ) + process { + $logLead = (Get-LogLeadName) + + $widgetType = 'Widget' + $serviceType = 'Service' + $migrationType = 'Migration' + $themeType = 'Theme' + $webApplicationType = 'WebApplication' + $webExtensionType = 'WebExtension' + $repositoryType = 'Repository' + $providerType = 'Provider' + $websiteType = 'WebSite' + $legacyUtilityType = 'LegacyUtility' + $hotfixType = 'Hotfix' + $apiComponentType = 'ApiComponent' + + # Get the current folder so we can put/look for things in the working folder if not specified + $CurrentWorkingFolder = (Get-Location).Path + + # The base name is like C:\git\Alkami.Services.Permissions\ <- I want "Alkami.Services.Permissions" + # The base name can also be derived from the csproj name later + # So if the folder is C:\git\perms with an Alkami.Services.Permissions.csproj we can absorb that as the basename later + # The reason for a basename is when we start supporting node or ruby, etc, they don't have an assemblyInfo + $baseName = (Split-Path $CurrentWorkingFolder -Leaf) + + if ([string]::IsNullOrWhiteSpace($Destination)) { + $Destination = $CurrentWorkingFolder + } + + # Target path is where we store the final file at + # Easier to work with non-relative paths. This gives us a full path. + $targetPath = (Resolve-Path $Destination).Path + + # Determine where we want to drop the file when we're done + # Ensure that our target path ends in the manifest filename + $defaultFilename = "AlkamiManifest.$($FileType.ToLower())" + + $targetItemIsContainer = (Get-Item $targetPath).PSIsContainer + + if (!$targetItemIsContainer -and ((Split-Path $targetPath -Leaf) -ne $defaultFilename)) { + Write-Warning "$logLead : The specified filename is not the expected filename. Matching to required filename." + $targetPath = (Join-Path (Split-Path $targetPath -Parent) $defaultFilename) + } + + # if the target path is a container (is not a file) + # append the filename + if ($targetItemIsContainer) { + $targetPath = (Join-Path $targetPath $defaultFilename) + } + + # Target path is now a file that we want to target + if (!$Force -and (Test-Path $targetPath)) { + Write-Warning "$logLead : There's already a file at $targetPath - Did you want to Test-AlkamiManifest this file instead? Use -Force to overwrite the existing file." + return + } + + Write-Verbose "$logLead : Getting CSProj" + if ([string]::IsNullOrWhiteSpace($ProjectLocation)) { + $ProjectLocation = $CurrentWorkingFolder + } + + if (!(Test-Path $ProjectLocation)) { + Write-Warning "$logLead : Project location does not exist, looking in current folder for a csproj" + $ProjectLocation = $CurrentWorkingFolder + } + + # If it's a folder, look in the folder + if ((Get-Item $ProjectLocation).PSIsContainer) { + $csprojFile = ((Get-ChildItem (Join-Path $ProjectLocation "*.csproj") -Recurse) | Sort-Object -Unique | Select-Object -First 1) + } else { + # Test path did exist, it wasn't a folder, use that + $csprojFile = (Get-Item $ProjectLocation) + } + + # This could be a node project, or just an empty folder with no content yet + if ($null -ne $csprojFile) { + $baseName = (Get-Item $csprojFile).BaseName + } + + $assemblyName = $baseName + $rootNamespace = $baseName + $creatorCode = "" + $areaName = "" + $framework = "framework" + $splitRoots = @() + + # If we have a csproj file then let's collect information from it + if ((![string]::IsNullOrWhiteSpace($csprojFile)) -and (Test-Path $csprojFile)) { + $xml = [xml](Get-Content $csprojFile.FullName) + + $rootNamespaces = @($xml.Project.PropertyGroup.RootNamespace) + if (![string]::IsNullOrWhiteSpace($rootNamespaces[0])) { + $rootNamespace = $rootNamespaces[0] + } + + $assemblyNames = @($xml.Project.PropertyGroup.AssemblyName) + if (![string]::IsNullOrWhiteSpace($assemblyNames[0])) { + $assemblyName = $assemblyNames[0] + } + + $targetFrameworks = @($xml.Project.PropertyGroup.TargetFramework) + if (![string]::IsNullOrWhiteSpace($targetFrameworks[0])) { + $targetFramework = $targetFrameworks[0] + } + + if ($targetFramework -match "core") { + $framework = "dotnetcore" + } else { + $framework = "framework" + } + + $splitRoots = $rootNamespace.Split('.') + $creatorCode = $splitRoots[0] + } else { + # Refactor when we have node project examples to emit something more generic + Write-Warning "$logLead : There is no csproj file at [$ProjectLocation|$csprojFile]. Please run this from within a project code folder, or supply a valid ProjectLocation" + } + + if ([string]::IsNullOrWhiteSpace($assemblyName) -and ![string]::IsNullOrWhiteSpace($rootNamespace)) { + $assemblyName = $rootNamespace + } + + if ([string]::IsNullOrWhiteSpace($rootNamespace) -and ![string]::IsNullOrWhiteSpace($assemblyName)) { + $rootNamespace = $assemblyName + } + + $sectionManifest = "" + + if ($Type -eq $hotfixType) { + Write-Verbose "Building Hotfix Section" + + $sectionManifest = @" + + REPLACEME + REPLACESERVERTIER + +"@ + } + + if($Type -eq $apiComponentType){ + Write-Verbose "Building ApiComponent Section" + + $sectionManifest = @" + + REPLACEME + +"@ + } + + if ($Type -eq $widgetType) { + Write-Verbose "Building Widget Section" + + if (!!$splitRoots) { + # Widget area names are always the last part of the project name + # Alkami.Client.Widgets.Lending = Lending + $areaName = $splitRoots[-1] + + # In the case of SDK they are actually: + # BCU.Client.Widgets.Lending = BCULending + $areaName = $splitRoots[0] + $splitRoots[-1] + } + + $sectionManifest = @" + + This is a displayable widget name for the UI + This is a displayable widget description for the UI + Client + $areaName + $rootNamespace + Desktop + + +"@ + } + + if ($Type -eq $serviceType) { + Write-Verbose "$logLead : Building Service Section" + + $jsonServiceManifestChunk = @" + + $assemblyName.Migrations + + + my_service_role + +"@ + $xmlServiceManifestChunk = @" + + + + + +"@ + + $useChunk = $xmlServiceManifestChunk + if ($FileType -eq 'Json') { + $useChunk = $jsonServiceManifestChunk + } + + $sectionManifest = @" + + $framework + $assemblyName + $useChunk + +"@ + } + + if ($Type -eq $legacyUtilityType) { + Write-Verbose "Building LegacyUtility Section" + + $sectionManifest = @" + + + true + +"@ + } + + if ($Type -eq $migrationType) { + Write-Verbose "Building Migration Section" + + $sectionManifest = @" + + FluentMigrator|Fluent + REPLACEME + + + $assemblyName + +"@ + } + + if ($Type -eq $themeType) { + Write-Verbose "Building Theme Section" + + $sectionManifest = @" + + + + +"@ + } + + if ($Type -eq $webApplicationType) { + Write-Verbose "Building Web Application Section" + + $sectionManifest = @" + + + $areaName + + false + +"@ + } + + if ($Type -eq $webExtensionType) { + Write-Verbose "Building Web Extension Section" + + $sectionManifest = @" + + Client + +"@ + } + + if ($Type -eq $repositoryType) { + Write-Verbose "Building Repository Section" + + $sectionManifest = @" + + Client + +"@ + } + + if ($Type -eq $websiteType) { + Write-Verbose "Building WebSite Section" + + $sectionManifest = @" + + $baseName + + +"@ + } + + if ($Type -eq $providerType) { + Write-Verbose "Building Provider Section" + + $sectionManifest = @" + + + + BankService|Bank|SchedulerService|CoreService|Core|NotificationService|SecurityManagementService|SecurityManagement|Security|Radium|Nag|NagConfigurationService|NagConfig|NagConfiguration|All + $rootNamespace + + + Connector + +"@ + } + + Write-Verbose "Building Manifest" + + $sdkSection = @" + + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + +"@ + + # Special case for SDK clients to get extra help + if ($creatorCode -eq "Alkami") { + $sdkSection = "" + } + + $manifestPayload = @" + + + 1.0 + + $creatorCode + $assemblyName + $Type$sdkSection + + +$sectionManifest + +"@ + + Write-Host "$logLead : Creating a new manifest file for a $FileType [$Type] at [$targetPath]" + + if ($FileType -eq 'Json') { + $manifestPayload = ([xml]$manifestPayload | ConvertFrom-Xml).PackageManifest | Format-Json + } + + Set-Content -Path $targetPath -Value $manifestPayload; + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.tests.ps1 new file mode 100644 index 0000000..8b74d78 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-AlkamiManifest.tests.ps1 @@ -0,0 +1,163 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-AlkamiManifest" { + + function New-FakeCsProj { + $sampleCsproj = @" + + + + AnyCPU + {deadbeef-dead-beef-dead-beef00000075} + Library + Properties + Alkami.MicroServices.Testing.FakeService + Alkami.MicroServices.Testing.FakeService + v4.7.2 + 512 + true + + + +"@ + + $testFile = "TestDrive:\Alkami.Microservices.Testing.FakeService.csproj" + Set-Content -Path $testFile -Value $sampleCsproj + } + + function New-FakeManifest{ + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + my_service_role + + +"@ + + $testFile = "TestDrive:\AlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "SUT"} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + + Context "When no csproj file is found" { + It "Creates a file" { + Mock -modulename $moduleForMock -CommandName Set-Content {} + Mock -modulename $moduleForMock -CommandName Write-Warning + + New-AlkamiManifest -Type "service" -Destination "testdrive:\" -ProjectLocation "testdrive:\" + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -ParameterFilter {$Message -like "*there is no csproj file*" } + Assert-MockCalled -CommandName Set-Content -Times 1 -Exactly -Scope It + } + } + + Context "When Attempting to create a manifest when one already exists" { + New-FakeCsProj + New-FakeManifest + + It "Returns if no force flag is supplied" { + + Mock -ModuleName $moduleForMock -CommandName Set-Content {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -ParameterFilter {$Message -like "*there's already a file*" } + + # testing on warning text isn't ideal, but we also check below that no files are created. + New-AlkamiManifest -Type "service" -Destination "testdrive:\" -ProjectLocation "testdrive:\" + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -ParameterFilter {$Message -like "*there's already a file*" } + Assert-MockCalled -CommandName Set-Content -Times 0 -Exactly -Scope It + } + + It "Generates a file if a force flag is supplied" { + Mock -ModuleName $moduleForMock -CommandName set-content {} + + # testing on warning text isn't ideal, but we also check below that no files are created. + New-AlkamiManifest -Type "service" -Destination "TestDrive:\" -ProjectLocation "testdrive:\" -Force + + Assert-MockCalled -CommandName Set-Content -Times 1 -Exactly -Scope It + } + } + Context "When attempting to create a new manifest"{ + It "Generates a File" { + New-FakeCsProj + New-AlkamiManifest -Type "service" -Destination "TestDrive:\" -ProjectLocation "testdrive:\" -force + $file = Get-Content -Path "TestDrive:\AlkamiManifest.xml" + + $file | Should -Not -BeNullOrEmpty + } + } + + Context "When Creating Widget Manifests" { + #TBD + } + + Context "When Creating Service Manifests" { + It "Generates a file with a ServiceManifest section"{ + New-FakeCsProj + New-AlkamiManifest -Type "service" -Destination "TestDrive:\" -ProjectLocation "testdrive:\" -force + $file = (Get-Content -Path "TestDrive:\AlkamiManifest.xml") + $file | Should -Not -BeNullOrEmpty + $fileXml = [xml]$file + $filexml.packageManifest.ServiceManifest | Should -Not -BeNullOrEmpty + } + } + + Context "When Creating Hotfix Manifests" { + It "Generates a file with an Hotfix section"{ + New-FakeCsProj + New-AlkamiManifest -Type "Hotfix" -Destination "TestDrive:\" -ProjectLocation "testdrive:\" -force + $file = (Get-Content -Path "TestDrive:\AlkamiManifest.xml") + $file | Should -Not -BeNullOrEmpty + $fileXml = [xml]$file + $filexml.packageManifest.HotfixManifest | Should -Not -BeNullOrEmpty + } + } + + Context "When Creating Api Component Manifests" { + It "Generates a file with an ApiComponent section"{ + New-FakeCsProj + New-AlkamiManifest -Type "ApiComponent" -Destination "TestDrive:\" -ProjectLocation "testdrive:\" -force + $file = (Get-Content -Path "TestDrive:\AlkamiManifest.xml") + $file | Should -Not -BeNullOrEmpty + $fileXml = [xml]$file + $filexml.packageManifest.ApiComponentsManifest | Should -Not -BeNullOrEmpty + } + } + + Context "When Creating Migration Manifests" { + #TBD + } + + Context "When Creating Theme Manifests" { + #TBD + } + + Context "When Creating Template Manifests" { + #TBD + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigConnectionString.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigConnectionString.ps1 new file mode 100644 index 0000000..f885b9a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigConnectionString.ps1 @@ -0,0 +1,87 @@ +function New-MachineConfigConnectionString { + <# +.SYNOPSIS + Create a new connectionStrings node with default values in machine.config + +.PARAMETER Use64Bit + Whether to modify the 64-bit machine.config or the 32-bit machine.config +#> + [CmdletBinding()] + Param( + [bool]$Use64Bit = $true + ) + + $logLead = Get-LogLeadName + + $mcPath = Get-DotNetConfigPath $use64Bit + $bitness = if ($Use64Bit) { "64" } else { "32" } + Write-Host ("$logLead : Checking ConnectionString Section in {0}-bit machine.config" -f $bitness) + + [XML]$machineConfig = Read-MachineConfig $Use64Bit + [System.Xml.XmlElement]$configRoot = $machineConfig.configuration + $machineConfigIsDirty = $false + + Write-Verbose "$logLead : Looking for AlkamiMaster connectionString node" + $masterNodes = $configRoot.SelectNodes("//connectionStrings/add[@name='AlkamiMaster']") + if ($null -eq $masterNodes -or $masterNodes.Count -eq 0) { + Write-Host "$logLead : AlkamiMaster Element not Found" + $connectionStringNode = $configRoot.SelectSingleNode("//connectionStrings") + if ($null -eq $connectionStringNode) { + Write-Output ("$logLead : Creating connectionStrings Element") + # Create the Connection String Element + $csElement = $machineConfig.CreateElement("connectionStrings") + } else { + # Select the Connection String Element + Write-Verbose "$logLead : connectionStrings Element Found" + $csElement = $machineConfig.SelectSingleNode("//connectionStrings") + } + + Write-Host "$logLead : Creating AlkamiMaster Element" + $csKey = $machineConfig.CreateElement("add") + $csName = $machineConfig.CreateAttribute("name") + $csName.Value = "AlkamiMaster" + + $csString = $machineConfig.CreateAttribute("connectionString") + + if ($masterConnectionString -ne "REPLACEME") { + $csString.Value = $masterConnectionString + } else { + Write-Warning "$logLead : The master connection string value is still set to REPLACEME. It will be added with an empty value and must be updated manually." + $csString.Value = "" + } + + $csProvider = $machineConfig.CreateAttribute("providerName") + $csProvider.Value = "System.Data.SqlClient" + + $csKey.Attributes.Append($csName) | Out-Null + $csKey.Attributes.Append($csString) | Out-Null + $csKey.Attributes.Append($csProvider) | Out-Null + + $csElement.AppendChild($csKey) | Out-Null + $configRoot.AppendChild($csElement) | Out-Null + $machineConfigIsDirty = $true + } else { + # Verify the Connection String Value + $alkamiNode = $configRoot.SelectNodes("//add[@name='AlkamiMaster']") + [System.Xml.XmlAttribute]$connectionString = ($alkamiNode.Attributes | Where-Object { $_.Name -eq "connectionString" } | Select-Object -First 1) + + Write-Verbose "$logLead : Checking the AlkamiMaster connectionString value" + if ($connectionString.Value -ne $masterConnectionString -and $masterConnectionString -ne "REPLACEME") { + Write-Host "$logLead : Updating the AlkamiMaster connectionString value" + # Set the Connection String Value + $connectionString.Value = $masterConnectionString + $machineConfigIsDirty = $true + } elseif ($masterConnectionString -eq "REPLACEME") { + Write-Warning "$logLead : The master connection string value is still set to REPLACEME. The value will not be updated" + } + } + + if ($machineConfigIsDirty) { + Write-Host "$logLead : Saving Modified machine.config" + $machineConfig.Save($mcPath) + } else { + Write-Host "$logLead : No changes required to the machine.config" + } +} + +Set-Alias -name Create-MachineConfigConnectionString -value New-MachineConfigConnectionString; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigMachineKeys.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigMachineKeys.ps1 new file mode 100644 index 0000000..721f671 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-MachineConfigMachineKeys.ps1 @@ -0,0 +1,58 @@ +function New-MachineConfigMachineKeys { + <# +.SYNOPSIS + Add new MachineKey nodes to machine.config +#> + [CmdletBinding()] + Param() + + $logLead = Get-LogLeadName + + [XML]$machineConfig = Read-MachineConfig + [System.Xml.XmlElement]$configRoot = $machineConfig.configuration + $machineConfigIsDirty = $false + + $config = $configRoot.SelectSingleNode("//system.web") + $machineKeyNode = $config.SelectSingleNode("//machineKey") + + if ($null -eq $machineKeyNode) { + Write-Host "$logLead : Creating machineKey Node" + $machineKeyNode = $machineConfig.CreateElement("machineKey") + $config.AppendChild($machineKeyNode) | Out-Null + } + + if ($null -eq $machineKeyNode.Attributes["validationKey"] -or [String]::IsNullOrEmpty($machineKeyNode.Attributes["validationKey"].Value)) { + $newKey = Get-MachineKeyValidationKey + Write-Verbose ("$logLead : Setting validationKey to {0}" -f $newKey) + $machineKeyNode.SetAttribute("validationKey", $newKey); + $machineConfigIsDirty = $true + } else { + # Prefer the existing value if it exists to avoid modifying the machine.config unnecessarily + Write-Warning "$logLead : A validation key is already set in the machine.config. Manually verify that the value is identical across the app\web tier servers" + } + + if ($null -eq $machineKeyNode.Attributes["decryptionKey"] -or [String]::IsNullOrEmpty($machineKeyNode.Attributes["decryptionKey"].Value)) { + $newKey = Get-MachineKeyDecryptionKey + Write-Verbose ("$logLead : Setting decryptionKey to {0}" -f $newKey) + $machineKeyNode.SetAttribute("decryptionKey", $newKey); + $machineConfigIsDirty = $true + } else { + # Prefer the existing value if it exists to avoid modifying the machine.config unnecessarily + Write-Warning "$logLead : A decryptionKey key is already set in the machine.config. Manually verify that the value is identical across the app\web tier servers" + } + + if ($machineKeyNode.Attributes["decryption"].Value -ne $decryptionMethod) { + Write-Verbose ("$logLead : Setting decryption to {0}" -f $decryptionMethod) + $machineKeyNode.SetAttribute("decryption", $decryptionMethod); + $machineConfigIsDirty = $true + } + + if ($machineConfigIsDirty) { + Write-Host "$logLead : Saving Modified machine.config" + $machineConfig.Save($machineConfigPath) + } else { + Write-Host "$logLead : No changes required to the machine.config" + } +} + +Set-Alias -name Create-MachineConfigMachineKeys -value New-MachineConfigMachineKeys; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-OrbSymLinks.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-OrbSymLinks.ps1 new file mode 100644 index 0000000..ba933ce --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-OrbSymLinks.ps1 @@ -0,0 +1,34 @@ +function New-OrbSymLinks { + <# + +.SYNOPSIS + Creates a SymLink between a orb folder and orb/shared + +.EXAMPLE + New-OrbSymLinks +#> + [CmdletBinding()] + [OutputType()] + param() + $logLead = Get-LogLeadName + $appFolderNames = Get-OrbSymLinkFolderNames + $orbPath = Get-OrbPath + + foreach ($appFolderName in $appFolderNames) { + + $appPath = Join-Path -Path $orbPath -ChildPath $appFolderName + $sharedPath = "$appPath\bin\shared" + $orbShared = Join-Path -Path $orbPath -ChildPath "shared" + + Write-Host "$logLead : testing path: $appPath" + if ((Test-Path $appPath) -eq $true) { + Write-Host "$logLead : testing symlink path: $sharedPath" + if ((Test-Path $sharedPath) -eq $false) { + Write-Host "$logLead : Creating SymLink: $sharedPath" + New-Symlink -ActualFilePath $orbShared -TargetFilePath $sharedPath + } + } else { + Write-Error "$loglead : Symlink source path not found: $appPath" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-RegistryKey.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-RegistryKey.ps1 new file mode 100644 index 0000000..52b73dc --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-RegistryKey.ps1 @@ -0,0 +1,37 @@ +function New-RegistryKey { +<# +.SYNOPSIS +Create a new registry key + +.DESCRIPTION +Creates key givin a path, does not create property for key. + +.EXAMPLE +New-RegistryKey -regKey HKCU:\Environment\foo -Verbose +#> + [cmdletbinding()] + param ( + $RegKey + ) + $logLead = Get-LogLeadName + $regKeyParent = Split-Path -Path $RegKey -Parent + $regKeyName = Split-Path -Path $RegKey -Leaf + + try { + Write-Verbose "$logLead : Testing for key's existance $RegKey" + + if (!(Test-RegistryKey $RegKey)) { + Write-Verbose "$logLead : Creating Registery Key $RegKey" + $regKeyResult = New-Item -Path $regKeyParent -Name $regKeyName + Write-Host "$logLead : Registry key created." + } else { + throw "$logLead : Registry key already exists." + } + } catch [System.Management.Automation.PSArgumentException] { + Write-Warning "$logLead : $RegKey exists." + } catch { + Write-Warning "$loglead : $_" + } + + return $regKeyResult +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-VIPsHostFileEntries.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-VIPsHostFileEntries.ps1 new file mode 100644 index 0000000..9d2b029 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-VIPsHostFileEntries.ps1 @@ -0,0 +1,52 @@ +function New-VIPsHostFileEntries { +<# +.SYNOPSIS + Creates Host File Entries for the Application VIPs using the Supplied IP Prefix +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$appTierVipPrefix + ) + + $logLead = (Get-LogLeadName); + $hostsContent = Get-HostsFileContent + $hostsToAdd = @() + + foreach ($app in $appTierApplications) { + # First try to resolve by name to see if DNS is handling + # We may be using this approach in AWS + try { + $addresses = Get-IPAddressesForName $app.Name + } + catch { + # No action needed, check is below + $addresses = $null + } + + if (!(Test-IsCollectionNullOrEmpty $addresses)) { + Write-Output ("$logLead : IP Address for App {0} Resolved" -f $app.Name) + continue; + } + + $hostsString = "{0}.{1}{2}{3}" + + # This is a redundant check since we're already checking DNS for the Service Name, but it doesn't hurt + if (($hostsContent | Where-Object {$_ -like ($hostsString -f "*", $app.Name)}).Count -gt 0) { + Write-Output ("$logLead : HostsEntry for {0} Already Exists" -f $app.Name) + continue + } + + $hostsToAdd += ($hostsString -f $appTierVipPrefix, $app.VIPSuffix, "`t`t", $app.Name) + } + + if ($hostsToAdd.Count -gt 0) { + Add-HostsFileContent $hostsToAdd + } + else { + Write-Output "$logLead : No Host File Updates Required" + } +} + +Set-Alias -name Create-VIPsHostFileEntries -value New-VIPsHostFileEntries; diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.ps1 new file mode 100644 index 0000000..6543bfd --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.ps1 @@ -0,0 +1,46 @@ +function New-WebTierHostFileEntries { + +<# +.SYNOPSIS + Creates a Host File Entry for the Supplied URL Pointed to the Loopback IP + +.DESCRIPTION + Creates a Host File Entry for the Supplied URL Pointed to the Loopback IP + +.PARAMETER Url + [string] DNS entry to put map to loopback in HOSTS file + +.NOTES + Target for future deprecation. This could be an inline func from the caller + +.EXAMPLE + New-WebTierHostFileEntries "www.foo.bar" + [Add-HostsFileContent] : Adding Hosts Entry : 127.0.0.1 www.foo.bar + [Add-HostsFileContent] : Saving Modified Hosts File to C:\WINDOWS\System32\Drivers\etc\hosts +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$Url + ) + + $logLead = (Get-LogLeadName); + + $hostsContent = Get-HostsFileContent + $hostsString = "127.0.0.1{0}{1}" + $cleanedUrl = (Format-Url -url $Url) + $hostsRegex = "^127\.0\.0\.1\s+$cleanedUrl" + + if (($hostsContent | Where-Object {$_ -match $hostsRegex }).Count -gt 0) { + + Write-Warning ("$logLead : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $cleanedUrl) + return + } + + $newHostsEntry = ($hostsString -f "`t`t", $url) + Write-Verbose "$logLead : Calling Add-HostsFileContent with -Force flag" + Add-HostsFileContent -contentToAdd $newHostsEntry -Force +} + +Set-Alias -name Create-WebTierHostFileEntries -value New-WebTierHostFileEntries; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.tests.ps1 new file mode 100644 index 0000000..7f6e27e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/New-WebTierHostFileEntries.tests.ps1 @@ -0,0 +1,116 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-WebTierHostFileEntries" { + + Mock -CommandName Get-HostsFileContent -ModuleName $moduleForMock -MockWith { + return @" +127.0.0.1 thisexistsalready.fakeplace.com +#127.0.0.1 thisiscommentedout.fakeplace.com +# 127.0.0.1 thisiscommentedout.fakeplace.com +127.0.0.2 someonemisconfiguredthiswebhost.fakeplace.com +127-0-0-1 badbutalmostthesameipmatchingurl.fakeplace.com +"@ + } + + It "NewEntry_Calls_Add-HostsFileContent_And_DoesNotWarn" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $newEntryUrl = "thisreallydoesnotexist.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $newEntryUrl) + $expectedHostsEntry = "127.0.0.1`t`t$newEntryUrl" + + New-WebTierHostFileEntries -Url $newEntryUrl + + Assert-MockCalled Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$contentToAdd -eq $expectedHostsEntry} + + } + It "NewEntryThatIsSubstringOfExistingEntry_Calls_Add-HostsFileContent_And_DoesNotWarn" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $newEntryUrl = "existsalready.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $newEntryUrl) + $expectedHostsEntry = "127.0.0.1`t`t$newEntryUrl" + + New-WebTierHostFileEntries -Url $newEntryUrl + + Assert-MockCalled Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$contentToAdd -eq $expectedHostsEntry} + + } + It "ExistingEntry_WritesMessage_And_DoesNotCall_Add-HostsFileContent" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $existingEntryUrl = "thisexistsalready.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $existingEntryUrl) + + New-WebTierHostFileEntries -Url $existingEntryUrl + + Assert-MockCalled Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + + } + It "CommentedEntry_Calls_Add-HostsFileContent_And_DoesNotWarn" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $commentedEntryUrl = "thisiscommentedout.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $commentedEntryUrl) + $expectedHostsEntry = "127.0.0.1`t`t$commentedEntryUrl" + + New-WebTierHostFileEntries -Url $commentedEntryUrl + + Assert-MockCalled Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$contentToAdd -eq $expectedHostsEntry} + + } + It "NonLoopback_MatchingUrl_Calls_Add-HostsFileContent_And_DoesNotWarn" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $nonLoopbackMatchingEntryUrl = "someonemisconfiguredthiswebhost.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $nonLoopbackMatchingEntryUrl) + $expectedHostsEntry = "127.0.0.1`t`t$nonLoopbackMatchingEntryUrl" + + New-WebTierHostFileEntries -Url $nonLoopbackMatchingEntryUrl + + Assert-MockCalled Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$contentToAdd -eq $expectedHostsEntry} + + } + It "SimilarIp_MatchingUrl_Calls_Add-HostsFileContent_And_DoesNotWarn" { + Mock -CommandName Get-LogLeadName -MockWith {return ""} -ModuleName $moduleForMock + Mock -CommandName Format-Url -MockWith {return $url} -ModuleName $moduleForMock + Mock -CommandName Add-HostsFileContent -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + + $similarIpUrl = "badbutalmostthesameipmatchingurl.fakeplace.com" + $expectedWarning = (" : LOOPBACK Hosts File Entry for URL {0} Already Exists" -f $similarIpUrl) + $expectedHostsEntry = "127.0.0.1`t`t$similarIpUrl" + + New-WebTierHostFileEntries -Url $similarIpUrl + + Assert-MockCalled Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$Message -eq $expectedWarning} + Assert-MockCalled Add-HostsFileContent -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter {$contentToAdd -eq $expectedHostsEntry} + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.ps1 new file mode 100644 index 0000000..f8f6e7c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.ps1 @@ -0,0 +1,70 @@ +function Remove-AppSetting { +<# +.SYNOPSIS + Removes an appSetting Key/Value pair in the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Remove an appSetting key/value pair in the specified config file if present. + Will default to the global 64 bit machine.config file if no config file value specified. + Will not tickle files where no values have changed. + Can connect to remote computers as well. + +.PARAMETER Key + [string] The appSetting key + +.PARAMETER FilePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER ComputerName + [string] The computer to connect to. Defaults to localhost + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Internal function uses the SupportsShouldProcess flag, not us, this is good and proper")] + param ( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost", + + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + $logLead = (Get-LogLeadName) + +#region guard clauses + # If a computername was provided, modify the filepath to be a UNC path. + if((![string]::IsNullOrWhiteSpace($ComputerName)) -and ($ComputerName -ne "localhost")) { + $FilePath = (Get-UncPath -filePath $FilePath -ComputerName $ComputerName) + } + + if (!(Test-Path -PathType Leaf -Path $FilePath)) { + Write-Warning "$logLead : Could not find a file at [$FilePath]. Execution cannot continue." + return ## exit early + } + + Write-Verbose "$logLead : Reading Config file at [$FilePath]" + $xml = Read-XMLFile $FilePath + if(!$xml) { + throw "$logLead : Config at [$FilePath] could not be converted to xml." + } + + Write-Verbose "$logLead : Ensuring configuration root node exists..." + if(!$xml.configuration){ + throw "$logLead : How does [$FilePath] not have a root element??" + } +#endregion guard clauses + + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($Key, $FilePath, $Force) -ScriptBlock { + param($Key, $FilePath, $Force) + Remove-AppSettingPrivate -Key $Key -FilePath $FilePath -Force:$Force + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.tests.ps1 new file mode 100644 index 0000000..818dcf4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Remove-AppSetting.tests.ps1 @@ -0,0 +1,152 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut + +# Have to do this to use the private function for Set-AppSettingPrivate +# Write-Host "Overriding SUT: $functionPath" +# Import-Module $functionPath -Force +$moduleForMock = "Alkami.PowerShell.Configuration" + +Describe "Remove-AppSetting" -Tag "Integration" { + + # Temp file to write content to + $fakeConfigFile = Join-Path $TestDrive "fake.config" + + New-Item -ItemType Directory $TestDrive -ErrorAction SilentlyContinue | Out-Null + Write-Host ("Using temp path: $TestDrive for tests") + + Context "Removes from both settings blocks two appSettings sections are present" { + + $appSettingsTestContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Remove-AppSetting -Path $fakeConfigFile -Key "aspnet:MaxJsonDeserializerMembers" -Force + + $value = (Get-AppSetting -Path $fakeConfigFile -key "aspnet:MaxJsonDeserializerMembers") + + It "value didn't exist to retrieve, should be `$null" { + $value | Should -Be $null + } + } + + Context "Does not throw on a bad path" { + + {Remove-AppSetting -Path "C:\TotallyNotARealPath\TotallyNotARealConfig.config" -Key "Not a real key value"} | Should -Not -Throw + } + + Context "Handles more than one value found" { + + $appSettingsTestContents = @" + + + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Remove-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "still got deleted" { + (Get-AppSetting -Path $fakeConfigFile -key "FakeSetting") | Should -Be $null + } + } + + Context "Perfectly happy with no appSettings node" { + + $appSettingsTestContents = @" + + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Remove-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "doesn't do anything if the key doesn't exist" { + (Get-AppSetting -Path $fakeConfigFile -key "FakeSetting") | Should -Be $null + } + } + + Context "Throws an error if the configuration Node doesn't exist" { + + $appSettingsTestContents = @" +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + It "can't work with a malformed file" { + {Remove-AppSetting -Path $fakeConfigFile -Key "FakeSetting"} | Should -Throw + } + } + + Context "App Setting Key Does Not Exist" { + + $appSettingsTestContents = @" + + + + +"@ + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Remove-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "when there is no existing key it doesn't break" { + Get-AppSetting -Path $fakeConfigFile -key "FakeSetting" | Should -Be $null + } + } + + Context "Does a happy path" { + + $appSettingsTestContents = @" + + + + + +"@ + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Remove-AppSetting -Path $fakeConfigFile -Key "FakeSetting" + + It "deleted what it should have" { + (Get-AppSetting -Path $fakeConfigFile -key "FakeSetting") | Should -Be $null + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Remove-EnvironmentVariable.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Remove-EnvironmentVariable.ps1 new file mode 100644 index 0000000..6ef16e2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Remove-EnvironmentVariable.ps1 @@ -0,0 +1,37 @@ +function Remove-EnvironmentVariable { +<# +.SYNOPSIS + Removes an environment variable. Must supply the store name, defaults to machine level. + +.PARAMETER Name + Removes the environment variable with the specified name. + +.PARAMETER StoreName + The appropriate store to put the variable in. Defaults to machine. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("Key")] + [string]$Name, + [Parameter(Mandatory = $true)] + [Alias("Store")] + [Alias("Location")] + [ValidateSet("Process","User","Machine")] + [string]$StoreName + ) + + $logLead = (Get-LogLeadName) + + # Setting the value to $null deletes it from the store + $Value = $null + + Write-Host "$logLead : Remove environment variable [$Name] in [$StoreName] store" + + [System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::$StoreName) + + if ($StoreName -ne "Process") { + Write-Warning "$logLead : Environment variable with name [$Name] removed form [$StoreName] but not from the [Process] store.`r`nThis value may still be visible to the executing process." + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Remove-NetshExcludedPortRange.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Remove-NetshExcludedPortRange.ps1 new file mode 100644 index 0000000..79f972d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Remove-NetshExcludedPortRange.ps1 @@ -0,0 +1,76 @@ +function Remove-NetshExcludedPortRange { +<# +.SYNOPSIS + This function is used to remove a specific starting and number of ports to the system. + This is a very thing wrapper around the netsh tool for removing IPv4 ports. + +.PARAMETER Start + This is the starting port number. + +.PARAMETER NumberOfPorts + This is the count of ports in the given range. + +.PARAMETER End + This is the end port number when providing a range. + +.EXAMPLE + Remove-NetshExcludedPortRange -Start 50 -End 55 + +.EXAMPLE + Remove-NetshExcludedPortRange -Start 50 -NumberOfPorts 6 +#> + [CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("StartPort")] + [int]$Start, + [Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')] + [ValidateNotNullOrEmpty()] + [Alias("Range")] + [int]$NumberOfPorts, + [Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')] + [ValidateNotNullOrEmpty()] + [Alias("EndPort")] + [int]$End + ) + + $logLead = (Get-LogLeadName) + + if ($Start -le 0) { + throw "$logLead : Start port value must be greater than 0" + } + + if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') { + if ($NumberOfPorts -eq $Start) { + throw "$logLead : NumberOfPorts can not equal the parameter for Start" + } + + if ($NumberOfPorts -le 0) { + throw "$logLead : NumberOfPorts value must be greater than 0" + } + } + + if ($PSCmdlet.ParameterSetName -eq 'EndPorts') { + if ($End -le $Start) { + throw "$logLead : End port value must be larger than Start port value" + } + + if ($End -le 0) { + throw "$logLead : End port value must be greater than 0" + } + + $NumberOfPorts = $End - $Start + 1 + } + + Write-Host "$logLead : Deleting ipv4/tcp excludedPortRange for Start port [$Start] for [$NumberOfPorts] ports" + $output = netsh int ip delete excludedportrange tcp $Start $NumberOfPorts + + if ($output -match "error") { + Write-Error "$logLead : Failed to set the requested range.`r`n$output" + return $false + } + + return $true +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Rename-NewLogConfig.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Rename-NewLogConfig.ps1 new file mode 100644 index 0000000..acd922f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Rename-NewLogConfig.ps1 @@ -0,0 +1,30 @@ +function Rename-NewLogConfig { +<# +.SYNOPSIS + Rename new.log4net.config to log4net.config +#> + [CmdletBinding()] + Param() + $logLead = Get-LogLeadName + + $foldersWithoutLog4Net = (Get-ChildItem $basePath | Where-Object {$_.PSIsContainer -eq $true} | Where-Object {(Get-ChildItem $_.FullName | Where-Object {$_.Name -eq "log4net.config"}).Count -eq 0}) + + if (Test-IsCollectionNullOrEmpty $foldersWithoutLog4Net) { + Write-Output ("$logLead : Found log4net.config in all folders under {0}" -f $basePath) + return + } + + foreach ($folder in $foldersWithoutLog4Net | Where-Object {$_.Name -notmatch "Shared"}) { + $newConfigPath = Join-Path $folder.FullName "new.log4net.config" + + if (!(Test-Path $newConfigPath)) { + Write-Warning ("$logLead : Folder {0} does not have log4net.config or a new.log4net.config. This needs manual review" -f $folder.FullName) + continue + } + + $outFile = Join-Path $folder.FullName "log4net.config" + Write-Output ("$logLead : Copying file {0} to {1}" -f $newConfigPath, $outFile) + [System.IO.File]::Copy($newConfigPath, $outFile) + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.ps1 new file mode 100644 index 0000000..de27749 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.ps1 @@ -0,0 +1,42 @@ +function Rename-TemporaryConfigFiles { +<# +.SYNOPSIS + Renames Temporary Configuration Files to remove the "new." prefix. For example, new.web.config is renamed to web.config +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string[]]$temporaryConfigFiles, + + [Parameter(Mandatory=$false)] + [Alias("Force")] + [switch]$forceOverwrite + ) + + $logLead = (Get-LogLeadName); + + foreach ($configfile in $temporaryConfigFiles) { + + [Regex]$newMatchRegex = "\\new\." + $finalConfigFileName = $newMatchRegex.Replace($configFile, "\") + + if (Test-Path $finalConfigFileName) + { + if ($forceOverwrite.IsPresent) + { + Write-Host ("$logLead : Config file '{0}' already exists and will be removed before rename." -f $finalConfigFileName) + Remove-Item "$finalConfigFileName" -Force + } + else + { + Write-Warning ("$logLead : Config file '{0}' already exists. No changes will be made. To overwrite the file, use the -Force parameter" -f $finalConfigFileName) + continue + } + } + + Write-Host ("$logLead : Renaming file '{0}' to '{1}'" -f $configFile, $finalConfigFileName) + Rename-Item "$configfile" "$finalConfigFileName" -Force:$forceOverwrite + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.tests.ps1 new file mode 100644 index 0000000..92dc26c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Rename-TemporaryConfigFiles.tests.ps1 @@ -0,0 +1,79 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Rename-TemporaryConfigFiles + +Describe Rename-TemporaryConfigFiles { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + $tempOrb = ($tempPath + "\ORB") + + if (!(Test-Path $tempOrb)) + { + New-Item -ItemType Directory $tempOrb | Out-Null + } + + Write-Warning ("Using temp path: $tempOrb for tests") + + # Make Directories for Tests + $bankServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\BankService") + $coreServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\CoreService") + + # Make Files for Tests + $newBankFile = (Join-Path $bankServiceDirectory "new.web.config") + "newFile" | Out-File $newBankFile + + $newCoreFile = (Join-Path $coreServiceDirectory "new.web.config") + "newFile" | Out-File $newCoreFile + + $originalCoreFile = (Join-Path $coreServiceDirectory "web.config") + "test" | Out-File $originalCoreFile + + It "Renames files without conflict" { + + $files = Get-ConfigurationFiles $tempOrb $true + Rename-TemporaryConfigFiles $files + + Test-Path $newBankFile | Should Be $false + + $bankFile = (Join-Path $bankServiceDirectory "web.config") + Test-Path $bankFile | Should Be $true + GC $bankFile | Should Match "newFile" + } + + It "Writes a warning when the file already exists" { + + $files = Get-ConfigurationFiles $tempOrb $true + { (Rename-TemporaryConfigFiles $files 3>&1) -match "No changes will be made" } | Should Be $true + } + + It "Does not overwrite the existing file if it already exits by default" { + + $files = Get-ConfigurationFiles $tempOrb $true + Rename-TemporaryConfigFiles $files + + Get-Content $originalCoreFile | Should Match "test" + Test-Path $newCoreFile | Should Be $true + } + + It "Overwrites the existing file if the Force parameter is supplied" { + + $files = Get-ConfigurationFiles $tempOrb $true + Rename-TemporaryConfigFiles $files -Force + + Test-Path $newCoreFile | Should Be $false + Test-Path $originalCoreFile | Should Be $true + GC $originalCoreFile | Should Match "newFile" + } +} + +#endregion Rename-TemporaryConfigFiles \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-AlkamiConfigs.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-AlkamiConfigs.ps1 new file mode 100644 index 0000000..0d01e70 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-AlkamiConfigs.ps1 @@ -0,0 +1,124 @@ +function Set-AlkamiConfigs { +<# +.SYNOPSIS + Set web.config default value if not exist else copy from exising +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$enviroment, + [Parameter(Mandatory=$false)] + [String]$tempOrbPath = "C:\temp\Deploy\Orb\", + [Parameter(Mandatory=$false)] + [String]$orbPath + ) + + $logLead = (Get-LogLeadName); + + if([string]::IsNullOrEmpty($orbPath)) { + $orbPath = (Get-OrbPath) + } + + # Set Config + If((!(Test-Path $orbPath -PathType Container)) -and (Test-Path $tempOrbPath -PathType Container)) { + + If(!(Test-Path $tempOrbPath -PathType Container)) { + Throw ("$logLead Folder not found '{0}'" -f $tempOrbPath) + } + + $tempOrbfolders = Get-ChildItem -Directory $tempOrbPath -Exclude "Shared" + $XmlNodeValues = @( + @{NodeString = "//appSettings/add[@key='ShouldDisplayUncaughtExceptions']"; AttributeName = "value"; AttributeValue = "false" }, + @{NodeString = "//system.web/compilation[@debug]"; AttributeName = "debug"; AttributeValue = "false" }, + @{NodeString = "//httpErrors"; AttributeName = "errorMode"; AttributeValue = "DetailedLocalOnly"}, + @{NodeString = "//authentication/forms[@cookieless]"; AttributeName = "cookieless"; AttributeValue = "UseCookies"}, + @{NodeString = "//appSettings/add[@key='ForceUseOfProductionAssetsInDebug']"; AttributeName = "value"; AttributeValue = "true"}, + @{NodeString = "//glimpse[@defaultRuntimePolicy]"; AttributeName = "defaultRuntimePolicy"; AttributeValue = "Off"}, + @{NodeString = "//customErrors"; AttributeName = "mode"; AttributeValue = "RemoteOnly"} + ) + + $configfiles = Get-ChildItem $tempOrbfolders -Recurse -Include "new.web.config","new.Alkami.App.Nag.Host.Service.exe.config","new.Alkami.App.Radium.WindowsService.exe.config" | Select-Object name -ExpandProperty Fullname + foreach ($configfile in $configfiles) { + $files = Split-Path $configfile -Leaf + $files = $files -replace("new.","") + $folders = Split-Path $configfile -Parent + $newFiles = Join-Path -Path $folders -ChildPath $files + if (!(Test-Path -Path $newFiles)) { + Rename-Item "$configfile" "$newFiles" + } + } + + foreach ($tempOrbfolder in $tempOrbfolders) { + $tempOrbConfig = Get-ChildItem $tempOrbfolder | Where-Object {$_.Name -match "^web.config|^Alkami.App.Radium.WindowsService.exe.config|^Alkami.App.Nag.Host.Service.exe.config"} | Select-Object -First 1 + Write-Host ("$logLead : Updating Config File '{0}'" -f $tempOrbConfig.fullname) + + [Xml]$doc = (Get-Content $tempOrbConfig.fullname) + foreach ($XmlNodeValue in $XmlNodeValues) { + + Write-Host ("$logLead : Setting {0} to {1}" -f $XmlNodeValue.NodeString, $XmlNodeValue.AttributeValue) + Set-XmlNodeValue ` + -NodeString $XmlNodeValue.NodeString ` + -AttributeName $XmlNodeValue.AttributeName ` + -AttributeValue $XmlNodeValue.AttributeValue | + Out-Null + } + + # Remove diagnostics + $ChildNodes = $doc.SelectNodes("//system.diagnostics") + Write-Host ("$logLead : Removing System Diagnostics Child Nodes") + if ($ChildNodes) { + + foreach($Child in $ChildNodes){ + $Child.ParentNode.RemoveChild($Child) + } + } + $doc.Save($tempOrbConfig.fullname) + } + } + + # Copy Config + If((Test-Path $orbPath -PathType Container) -and (Test-Path $tempOrbPath -PathType Container)){ + Write-Host ("$logLead : Copying Configuration Values from {0} to {1}" -f $orbPath,$tempOrbPath) + $orbfolders = Get-ChildItem -Directory $tempOrbPath -Exclude "Shared" + $subfolders = Split-Path $orbfolders -Leaf -Resolve + + foreach ($subfolder in $subfolders) { + $tempConfig = Get-ChildItem (Join-Path $tempOrbPath $subfolder) | where-Object {$_.Name -match "^web.config|^Alkami.App.Radium.WindowsService.exe.config|^Alkami.App.Nag.Host.Service.exe.config"} + $orbConfig = Get-ChildItem (Join-Path $orbPath $subfolder) | where-Object {$_.Name -match "^web.config|^Alkami.App.Radium.WindowsService.exe.config|^Alkami.App.Nag.Host.Service.exe.config"} + Write-Host ("$logLead : Setting node value from '{0}' to '{1}'" -f $orbConfig.FullName,$tempConfig.FullName) + [Xml]$orbXml = Get-Content $orbConfig.FullName + [Xml]$tempXml = Get-Content $tempConfig.FullName + + $nodeStrings = ("//appSettings","//quartz","//glimpse") + foreach ($nodeString in $nodeStrings) { + $orb = $orbXml.SelectSingleNode($nodeString) + $temp = $tempXml.SelectSingleNode($nodeString) + foreach ($tempNode in $temp.ChildNodes) { + if ($tempNode.NodeType -ne "Comment") { + $orbKey = $orb.ChildNodes| Where-Object {$_.Key -eq $tempNode.Key} | Select-Object -First 1 + if($orbKey.key) { + Write-Host ("$logLead : Setting Node '{0}' to value '{1}'. Default value was: '{2}" -f $orbKey.Key, $orbKey.Value, $tempNode.Value) + $tempNode.value = $orbKey.value + } + else + { + Write-Host ("$logLead : Key {0} Not Present in the New File -- It Will be Dropped. Previous value was: {1}" -f $orbKey.Key, $orbKey.Value) + } + } + } + } + + $tempXml.Save($tempConfig.FullName) + } + } + + # Update New Relic application names. + if(Test-Path $tempOrbPath) + { + $configFiles = @(); + $configFiles += Get-ConfigurationFiles -stagedFilePath $tempOrbPath -findTempFiles $true; + $configFiles += Get-ConfigurationFiles -stagedFilePath $tempOrbPath -findTempFiles $false; + Write-Verbose ("$logLead : Calling Set-NewRelicAppName With Environment Key '{0}'" -f $enviroment); + Set-NewRelicAppName -enviroment $enviroment -configFiles $configfiles; + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.json.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.json.tests.ps1 new file mode 100644 index 0000000..7427961 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.json.tests.ps1 @@ -0,0 +1,183 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.json\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Get-Module $sut | Remove-Module -Force +$moduleForMock = "Alkami.PowerShell.Configuration" + +Describe "Set-AppSetting.json" { + $configPathUUT = "TestDrive:\appsettings.json" + + Context "Writes an Error and Returns False if the appSettings Node can't take children elements" { + + $appSettingsTestContents = @" +{ + "FakeSetting": "FakeValue" +} +"@ + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $configPathUUT -Force + + Set-AppSetting -Path $configPathUUT -Key "FakeSetting:SubSetting" -Value 'test' + + It "did write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 1 -Scope Context + } + + It "did not write a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 0 -Scope Context + } + } + + Context "App Setting Key Does Not Exist with -UpdateOnly" { + + $appSettingsTestContents = @" +{ +} +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $configPathUUT -Force + + Set-AppSetting -Path $configPathUUT -Key "MissingSetting" -Value 'test' -UpdateOnly + + It "does not create a missing AppSetting key with -UpdateOnly" { + $value | Should -BeNull + } + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "did write a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 0 -Scope Context + } + } + + Context "Does a happy path - simple" { + + $appSettingsTestContents = @" +{ + "FakeSetting": "FakeValue" +} +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $configPathUUT -Force + + Set-AppSetting -Path $configPathUUT -Key "FakeSetting" -Value 'test' + + $value = Get-AppSetting -Path $configPathUUT -key "FakeSetting" + + It "still got the right value" { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "Wrote no warnings" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "Showed simple verification" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 2 -Scope Context + } + } + + Context "Does a happy path - complex" { + + $appSettingsTestContents = @" +{ + "FakeSetting": { "FakeSubchild": "FakeValue" } +} +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $configPathUUT -Force + + Set-AppSetting -Path $configPathUUT -Key "FakeSetting:FakeSubchild" -Value 'test' + + $value = Get-AppSetting -Path $configPathUUT -key "FakeSetting:FakeSubchild" + + It "still got the right value" { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "Wrote no warnings" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "Showed simple verification" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 2 -Scope Context + } + } + + Context "Does a happy path - more complex" { + + $appSettingsTestContents = @" +{ + "FakeSetting": { "FakeMiddle": { "FakeSubchild": "FakeValue" } } +} +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $configPathUUT -Force + + Set-AppSetting -Path $configPathUUT -Key "FakeSetting:FakeMiddle:FakeSubchild" -Value 'test' + + $value = Get-AppSetting -Path $configPathUUT -key "FakeSetting:FakeMiddle:FakeSubchild" + + It "still got the right value" { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "Wrote no warnings" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "Showed simple verification" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 2 -Scope Context + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.ps1 new file mode 100644 index 0000000..1f44d9a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.ps1 @@ -0,0 +1,119 @@ +function Set-AppSetting { +<# +.SYNOPSIS + Sets an appSetting Key/Value pair in the specified file. Filepath defaults to the 64 bit machine config. + +.DESCRIPTION + Set an appSetting key/value pair in the specified config file. + Will default to the global 64 bit machine.config file if no config file value specified. + Will not tickle files where no values have changed. + Can connect to remote computers as well. + +.PARAMETER key + [string] The appSetting key + +.PARAMETER value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER filePath + [string] The location to change settings in. Defaults to the global 64bit machine.config file. + +.PARAMETER ComputerName + [string] The computer to connect to. Defaults to localhost + +.PARAMETER Force + [switch] Allow forcing the write of the process. This is due to the option for ShouldProcess. + +.PARAMETER UpdateOnly + [switch] Only updates a key's value if it exists. Will not create the missing AppSetting key. +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact='None')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Internal function uses the SupportsShouldProcess flag, not us, this is good and proper")] + param ( + [Parameter(Mandatory = $true)] + [string]$key, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$value, + + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string]$FilePath = (Get-DotNetConfigPath -use64Bit $true), + + [Parameter(Mandatory = $false)] + [string]$ComputerName = "localhost", + + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$UpdateOnly + ) + + $logLead = (Get-LogLeadName) + +#region guard clauses + # If a computername was provided, modify the filepath to be a UNC path. + if((![string]::IsNullOrWhiteSpace($ComputerName)) -and ($ComputerName -ne "localhost")) { + $FilePath = (Get-UncPath -filePath $FilePath -ComputerName $ComputerName) + } + + if (!(Test-Path -PathType Leaf -Path $FilePath)) { + Write-Warning "$logLead : Could not find a file at [$FilePath]. Execution cannot continue." + return ## exit early + } + + Write-Verbose "$logLead : Reading Config file at [$FilePath]"; + $isLikelyXml = $false + $isLikelyJson = $false + $fileContent = Get-Content -Path $FilePath -Raw + $firstCharacter = $fileContent[0] + + if ($firstCharacter -eq '<') { + $isLikelyXml = $true + } + + if (($firstCharacter -eq '{') -or ($firstCharacter -eq '[')) { + $isLikelyJson = $true + } +#endregion guard clauses + + if ($isLikelyXml) { + $xml = [xml]$fileContent + if(!$xml) { + throw "$logLead : Config at [$FilePath] expected to be xml but could not be parsed as xml." + } + + Write-Verbose "$logLead : Ensuring configuration root node exists..."; + if(!$xml.configuration){ + throw "$logLead : How does $FilePath not have a root element??" + } + + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($key, $value, $filePath, $Force, $UpdateOnly) -ScriptBlock { + param($key, $value, $filePath, $Force, $UpdateOnly) + Set-AppSettingPrivateXml -key $key -value $value -FilePath $filePath -Force:$Force -UpdateOnly:$UpdateOnly + } + + return + } + + if ($isLikelyJson) { + try { + # See if we can parse this as a json object + (ConvertFrom-Json -InputObject $fileContent) | Out-Null + } catch { + Write-Host "$logLead : $($_.Exception.Message)" + throw "$logLead : Config at [$FilePath] expected to be json but could not be parsed as json." + } + + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($key, $value, $filePath, $Force, $UpdateOnly) -ScriptBlock { + param($key, $value, $filePath, $Force, $UpdateOnly) + Set-AppSettingPrivateJson -Key $key -value $value -FilePath $filePath -Force:$Force -UpdateOnly:$UpdateOnly + } + + return + } + + throw "$logLead : Could not process file [$FilePath] as presented. Does not appear to be a valid file type" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.tests.ps1 new file mode 100644 index 0000000..7fb812f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.tests.ps1 @@ -0,0 +1,75 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut + +# Have to do this to use the private function for Set-AppSettingPrivate +# Write-Host "Overriding SUT: $functionPath" +# Import-Module $functionPath -Force +$moduleForMock = "Alkami.PowerShell.Configuration" + +Describe "Set-AppSetting" { + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + $fakeConfigFile = Join-Path $tempPath "fake.config" + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Host ("Using temp path: $tempPath for tests") + + Context "`$null check still works if no file found at path" { + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $value = Set-AppSetting -Path "C:\TotallyNotARealPath\TotallyNotARealConfig.config" -Key "Not a real key value" -value "Not a real value" + + It "still got the right value (null)" { + $value | Should -BeNull + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 0 -Scope Context + } + } + + Context "Throws an error if the configuration Node doesn't exist" { + + $appSettingsTestContents = @" +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + {Set-AppSetting -Path $fakeConfigFile -Key "FakeSetting" -Value "FakeValue"} | Should -Throw + + It "did not write an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "wrote two paths" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 0 -Scope Context + } + } + + Remove-Item $fakeConfigFile -Force + Remove-Item $tempPath -Force +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.xml.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.xml.tests.ps1 new file mode 100644 index 0000000..0503ad2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-AppSetting.xml.tests.ps1 @@ -0,0 +1,288 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.xml\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Get-Module $sut | Remove-Module -Force + +# Have to do this to use the private function for Set-AppSettingPrivate +# Write-Host "Overriding SUT: $functionPath" +# Import-Module $functionPath -Force +$moduleForMock = "Alkami.PowerShell.Configuration" +$moduleForMock2 = "Alkami.PowerShell.Common" + +Describe "Set-AppSetting - xml" { + #TODO: Refactor - mock Get-AppSetting, mock Read-XmlFile, mock Save-XmlFile + # Mock all the things - See "Get-AppSetting" tests for faking Read-XmlFile results + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + $fakeConfigFile = Join-Path $tempPath "fake.config" + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Host ("Using temp path: $tempPath for tests") + + Mock -ModuleName $moduleForMock2 -CommandName Write-Host -MockWith {} + + Context "Reports an error when two appSettings sections are present" { + + $appSettingsTestContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "aspnet:MaxJsonDeserializerMembers" -Value 50000 + + $value = Get-AppSetting -Path $fakeConfigFile -key "aspnet:MaxJsonDeserializerMembers" + + It "still got the right value" { + $value | Should Be "50000" + } + + It "did throw an error" { + # Once in Get-AppSetting and once in Set-AppSetting because of the duplicate block + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 2 -Scope Context + } + + It "wrote two warnings to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 3 -Scope Context + } + + It "wrote four found paths" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 8 -Scope Context + } + } + + Context "Writes a warning and updates all values if more than one setting exists" { + + $appSettingsTestContents = @" + + + + + + +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "FakeSetting" -Value "DuplicateResolved" + + It "the file looks accurate" { + ## The reason for this joins shenanigans is because Get-Content returns an array of lines, and I don't want a wrapping construct here. This is fine. thisisfine.gif + ((Get-Content $fakeConfigFile) -join ' ') | Should -Be ' ' + } + + $value = Get-AppSetting -Path $fakeConfigFile -key "FakeSetting" + + It "still got the right value" { + $value | Should -Be "DuplicateResolved" + } + + It "did not throw an error" { + # Once in Get-AppSetting and once in Set-AppSetting because of the duplicate block + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 2 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context + } + + It "wrote two paths" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 4 -Scope Context + } + } + + Context "Writes a Warning and Returns Null if the appSettings Node Doesn't Exist" { + + $appSettingsTestContents = @" + + + + + +"@ + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "FakeSetting" -Value 'test' + + $value = Get-AppSetting -Path $fakeConfigFile -key "FakeSetting" + + It "still got the right value" { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 1 -Scope Context + } + } + + Context "App Setting Key Does Not Exist" { + + $appSettingsTestContents = @" + + + + +"@ + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "FakeSetting" -Value 'test' + $value = Get-AppSetting -Path $fakeConfigFile -key "FakeSetting" + + It "creates a missing AppSetting key." { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 1 -Scope Context + } + } + + Context "App Setting Key Does Not Exist with -UpdateOnly" { + + $appSettingsTestContents = @" + + + + + +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "MissingSetting" -Value 'test' -UpdateOnly + $value = Get-AppSetting -Path $fakeConfigFile -key "MissingSetting" + + It "does not create a missing AppSetting key with -UpdateOnly" { + $value | Should -Be $null + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + # The one warning is from Get-AppSetting and not Set-AppSetting + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 1 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 1 -Scope Context + } + } + + Context "Does a happy path" { + + $appSettingsTestContents = @" + + + + + +"@ + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + $appSettingsTestContents | Out-File $fakeConfigFile -Force + + Set-AppSetting -Path $fakeConfigFile -Key "FakeSetting" -Value 'test' + + $value = Get-AppSetting -Path $fakeConfigFile -key "FakeSetting" + + It "still got the right value" { + $value | Should -Be 'test' + } + + It "did not throw an error" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Exactly -Times 0 -Scope Context + } + + It "wrote a warning to the console" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Exactly -Times 0 -Scope Context + } + + It "did not output useless information" { + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Host -Exactly -Times 1 -Scope Context + } + } + + Remove-Item $fakeConfigFile -Force + Remove-Item $tempPath -Force +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-BeaconFeatureSettings.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-BeaconFeatureSettings.ps1 new file mode 100644 index 0000000..1d60ff3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-BeaconFeatureSettings.ps1 @@ -0,0 +1,113 @@ +function Set-BeaconFeatureSettings{ +<# +.SYNOPSIS +Sets the Beacon/Feature settings in machine config. + +.DESCRIPTION +Removes current beacon settings in configuration.appSettings +and adds in the given settings. + +.PARAMETER EnvironmentName +String, the environment name with which to set the beacon settings. + +.PARAMETER EnvironmentType +String, the environment type with which to set the beacon settings. + +.PARAMETER EnvironmentHosting +String, the hosting environment with which to set the beacon settings. + +.PARAMETER EnvironmentServer +String, the server environment with which to set the beacon settings. + +.PARAMETER use64bit +Boolean, passed to Get-DotNetConfigPath - if set to true, will return the filepath +of the 64 bit machine config file. + +.EXAMPLE + +Set-BeaconFeatureSettings -EnvironmentName "Lane B" -EnvironmentType Build -EnvironmentHosting Aws -EnvironmentServer Nag + +Base usage, will set the beacon settings in configuration.appSettigns of the machineConfig file to + + + + + + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True)] + [Alias("Name", "Lane", "Pod")] + [string]$EnvironmentName, + + [Parameter(Mandatory=$True)] + [ValidateSet("Development","TeamQA","QA","Build","Secure","Staging","Production")] + [Alias("Type")] + [string]$EnvironmentType, + + [Parameter(Mandatory=$True)] + [ValidateSet("Firehost","OnPremise","Aws","Azure","Other")] + [Alias("Hosting")] + [string]$EnvironmentHosting, + + [Parameter(Mandatory=$True)] + [ValidateSet("All","Web","App","Sql","Radium","Nag","Other")] + [Alias("Server")] + [string]$EnvironmentServer, + + [Parameter(Mandatory=$False)] + [bool]$use64Bit = $True + ) + begin{ + $logLead = (Get-LogLeadName); + + Write-Verbose "$logLead Getting Maching.Config path, use64Bit set to $use64Bit" + $machineConfigPath = Get-DotNetConfigPath -use64Bit $use64Bit + if(!$machineConfigPath){throw "Machine config path could not be found"} + + Write-Verbose "$logLead Reading machine config file located at $machineConfigPath" + $machineConfig = Read-XMLFile $machineConfigPath + if(!$machineConfig){throw "Machine config at $machineConfigPath could not be converted to xml"} + + Write-Verbose "$logLead Constructing redis xml string" + $BeaconSettingsXmlString = @" + + + + + + +"@ + } + process{ + Write-Verbose "$logLead Ensuring configuration and appSetting nodes exist" + if(!$machineConfig.configuration){ + [void]$machineConfig.AppendChild($machineConfig.CreateNode("element","configuration", $null)) + } + if(!$machineConfig.configuration.appSettings){ + [void]$machineConfig.SelectSingleNode("configuration").AppendChild($machineConfig.CreateElement("appSettings")) + } + + Write-Verbose "$logLead Initializing new beacon settings doc" + $BeaconSettingsXmlDoc = [xml]($BeaconSettingsXmlString) + + Write-Verbose "$logLead Removing current beacon settings" + $appSettings = $machineConfig.configuration.SelectSingleNode("appSettings") + if($appSettings.SelectNodes("add").count -gt 0){ + $CurrentBeaconSettings = $appSettings.add | Where-Object {$BeaconSettingsXmlDoc.beaconSettings.add.key.Contains($_.key)} + $CurrentBeaconSettings | ForEach-Object {[void]$appSettings.RemoveChild($_)} + } + + Write-Verbose "$logLead Setting new beacon settings" + $BeaconNode = $machineConfig.ImportNode($BeaconSettingsXmlDoc.FirstChild, $true) + $BeaconNode.add | ForEach-Object {[void]$appSettings.AppendChild($_)} + + Write-Verbose "$logLead Saving machine config to path $machineConfigPath" + $machineConfig.Save($machineConfigPath) + } +} + + +#region Private Functions + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshIPListens.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshIPListens.ps1 new file mode 100644 index 0000000..2921c3e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshIPListens.ps1 @@ -0,0 +1,50 @@ +function Set-DefaultNetshIPListens { +<# +.SYNOPSIS + This function will ensure the specifically requested Alkami listeners are added to the Windows netsh repository +#> +# Unit testing note: This is not unit tested because it relies on the barebones system call implementation on netsh + [CmdletBinding()] + [OutputType([bool])] + param( + ) + + $logLead = (Get-LogLeadName) + + $envVarName = "ALKAMI.SRE.EXCLUDED_PORT_RANGE_CONFIGURED.IPLISTEN" + + # If the environment key is already set, then we don't need to configure this. + if ($null -eq (Get-EnvironmentVariable -Name $envVarName -StoreName Machine)) { + $listens = @(Get-NetshHttpIPListens) + + $output = "" + $eitherFailed = $false + if (!($listens -contains '0.0.0.0')) { + $output = netsh http add iplisten 0.0.0.0 + } + + if ($output -match "error") { + Write-Error "Could not set the iplisten for 0.0.0.0`r`n$output" + $eitherFailed = $true + } + + $output = "" + if (!($listens -contains '::')) { + $output = netsh http add iplisten :: + } + + if ($output -match "error") { + Write-Error "Could not set the iplisten for ::`r`n$output" + $eitherFailed = $true + } + + if ($eitherFailed) { + return $false + } + + Write-Host "$logLead : Successfully added IPListeners" + Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine + + return $true + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshURLACLS.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshURLACLS.ps1 new file mode 100644 index 0000000..15b6392 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultNetshURLACLS.ps1 @@ -0,0 +1,50 @@ +function Set-DefaultNetshURLACLS { +<# +.SYNOPSIS + This function registers the default system URLACL for netsh +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $portList = @("50002","50003") + $success = $true + $didSetEnvironment = $false + + foreach($port in $portList) { + $envVarName = "ALKAMI.SRE.EXCLUDED_PORT_RANGE_CONFIGURED.URLACL.$port" + + if ($null -eq (Get-EnvironmentVariable -Name $envVarName -StoreName Machine)) { + Write-Host "$logLead : Setting URLACL for Subscription Service" + + # This netsh match is very brute-force but we shouldn't have to do it often + # There's a better/cleaner way to do this where we parse each chunk into an object + # We then have to compare for each of the properties we want and maybe recreate it + # The odds of that are too small for the effort invested, so we keep the brute-force below + + # TODO Extract this into unit-testable external function? + $output = "" + if (!((netsh http show urlacl url="http://+:$port/" | Out-String) -match "SDDL")) { + $output = netsh http add urlacl url="http://+:$port/" sddl="D:(A;;GX;;;WD)" + } + + if ($output -match "error") { + Write-Error "$logLead : Could not set the URLACL for $port`r`n$output" + $success = $false + } else { + Write-Host "$logLead : Successfully added UrlAcl for $port" + Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine + } + + $didSetEnvironment = $true + } + } + + # If we set the environment, return the results + if($didSetEnvironment) { + return $success + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultTLSVersion.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultTLSVersion.ps1 new file mode 100644 index 0000000..2abf3d3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-DefaultTLSVersion.ps1 @@ -0,0 +1,24 @@ +function Set-DefaultTLSVersion { +<# +.SYNOPSIS + Adds registry keys that set .NET to use TLS 1.2 by default +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + $baseKey = "HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" + $base86Key = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319" + + $tlsSetting = "SystemDefaultTlsVersions" + $tlsDWord = 1 + + Write-Output ("$logLead : Adding setting {0} to key {1} with value {2}" -f $tlsSetting, $baseKey, $tlsDWord) + Set-ItemProperty -Path $baseKey -Name "$tlsSetting" -Value $tlsDWord -ErrorAction SilentlyContinue | Out-Null + + Write-Output ("$logLead : Adding setting {0} to key {1} with value {2}" -f $tlsSetting, $base86Key, $tlsDWord) + Set-ItemProperty -Path $base86Key -Name "$tlsSetting" -Value $tlsDWord -ErrorAction SilentlyContinue | Out-Null +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EagleEyePermissions.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EagleEyePermissions.ps1 new file mode 100644 index 0000000..20a6032 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EagleEyePermissions.ps1 @@ -0,0 +1,82 @@ +function Set-EagleEyePermissions { + <# + .SYNOPSIS + Sets the authorizedGroupsByOperation section values in the EagleEye web.config file + .PARAMETER bustCacheGroups + The comma separated list of security groups which should have Bust Cache permissions + .PARAMETER elevateLoggingGroups + The comma separated list of security groups which should have Elevate Logging permissions + .PARAMETER manipulateServiceGroups + The comma separated list of security groups which should have Manipulate Service Instance permissions + .PARAMETER defaultWebConfigLocation + The location of the EagleEye web.config file. Defaults to "C:\ProgramData\chocolatey\lib\Alkami.EagleEye\tools\web.config" + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$bustCacheGroups, + [Parameter(Mandatory = $true)] + [string]$elevateLoggingGroups, + [Parameter(Mandatory = $true)] + [string]$manipulateServiceGroups, + [Parameter(Mandatory = $false)] + $defaultWebConfigLocation + ) + + $logLead = (Get-LogLeadName); + + #Set $defaultWebConfigLocation default + if ([string]::IsNullOrEmpty($defaultWebConfigLocation)) { + $chocoInstallPath = Get-ChocolateyInstallPath + $defaultWebConfigLocation = Join-Path $chocoInstallPath "lib\Alkami.EagleEye\tools\web.config" + } + + [HashTable[]]$groupHash = @( + @{ Key = "BustCache"; Value = $bustCacheGroups }, + @{ Key = "ElevateLogging"; Value = $elevateLoggingGroups }, + @{ Key = "ManipulateServiceInstance"; Value = $manipulateServiceGroups } + ) + + Write-Verbose ("$logLead : Checking for web.config at {0}" -f $defaultWebConfigLocation) + if (!(Test-Path $defaultWebConfigLocation)) { + Write-Output ("$logLead : EagleEye doesn't seem to be installed on this machine") + return + } + + Write-Verbose ("$logLead : Attempting to read the web.config from {0}" -f $defaultWebConfigLocation) + [XML]$eagleEyeConfig = Get-Content $defaultWebConfigLocation -ErrorAction SilentlyContinue + + if ($null -eq $eagleEyeConfig) { + Write-Error ("$logLead : Could Not Read the EagleEye configuration from {0}" -f $defaultWebConfigLocation) + return + } + + $authorizedGroupsXPath = "//authorizedGroupsByOperation" + $authorizedGroupsSection = $eagleEyeConfig.SelectNodes($authorizedGroupsXPath) + + if ($null -eq $authorizedGroupsSection) { + Write-Error ("$logLead : Could not find a the authorized groups section with XPath {0}" -f $authorizedGroupsXPath) + return + } + + $targetSection = $authorizedGroupsSection | Select-Object -First 1 + foreach ($group in $groupHash) { + $childNode = $targetSection.ChildNodes | Where-Object { $_.Key -eq $group.Key } + + if ($null -eq $childNode) { + Write-Error ("$logLead : Unable to find a child node with Key {0}" -f $group.Key) + return + } + elseif ($childNode.Value -eq $group.Value) { + Write-Output ("$logLead : Authorized group section {0} already has correct value {1}" -f $group.Key, $group.Value) + continue + } + + Write-Output ("$logLead : Setting authorized group section {0} to value {1}" -f $group.Key, $group.Value) + $childNode.SetAttribute("value", $group.value) + } + + Write-Verbose ("$logLead : Saving modified web.config XML to {0}" -f $defaultWebConfigLocation) + $eagleEyeConfig.Save($defaultWebConfigLocation) +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentDesignation.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentDesignation.ps1 new file mode 100644 index 0000000..590c349 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentDesignation.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentDesignation { +<# + +.SYNOPSIS + Get the App Setting of Environment.Designation + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Designation" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentHosting.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentHosting.ps1 new file mode 100644 index 0000000..a2a6e16 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentHosting.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentHosting { +<# + +.SYNOPSIS + Get the App Setting of Environment.Hosting + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Hosting" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentName.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentName.ps1 new file mode 100644 index 0000000..a79cf3e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentName.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentName { +<# + +.SYNOPSIS + Get the App Setting of Environment.Name + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Name" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentNameSafeDesignation.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentNameSafeDesignation.ps1 new file mode 100644 index 0000000..b51b13d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentNameSafeDesignation.ps1 @@ -0,0 +1,42 @@ +function Set-EnvironmentNameSafeDesignation { +<# + +.SYNOPSIS + Get the App Setting of Environment.NameSafeDesignation + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.NameSafeDesignation" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + + # Adding environment variable from SRE-17307 + Set-EnvironmentVariable -Name "ALKAMI_ENVIRONMENT_NAME" -Value $Global:Name_Safe_Designation -StoreName "Machine" + + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentServer.ps1 new file mode 100644 index 0000000..4c7734a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentServer.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentServer { +<# + +.SYNOPSIS + Get the App Setting of Environment.Server + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Server" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentType.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentType.ps1 new file mode 100644 index 0000000..78ea573 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentType.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentType { +<# + +.SYNOPSIS + Get the App Setting of Environment.Type + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.Type" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentUserPrefix.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentUserPrefix.ps1 new file mode 100644 index 0000000..dc6c5be --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentUserPrefix.ps1 @@ -0,0 +1,38 @@ +function Set-EnvironmentUserPrefix { +<# + +.SYNOPSIS + Get the App Setting of Environment.UserPrefix + +.PARAMETER Value + [string] The appSetting value to set, if it's different than the existing file + +.PARAMETER ComputerName + [string] The computer to connect to Defaults to localhost + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Value, + [Parameter(Mandatory = $false)] + [string]$ComputerName + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Warning "$logLead : ComputerName is null or whitespace, assigning 'localhost' to ComputerName" + $ComputerName = 'localhost' + } + + $params = @{ + Key = "Environment.UserPrefix" + Value = $Value + ComputerName = $ComputerName + } + + $appSetting = Set-AppSetting @params + Write-Verbose "$logLead : ComputerName: [$ComputerName] App Setting value: [$Value]" + return $appSetting +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentVariable.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentVariable.ps1 new file mode 100644 index 0000000..7038938 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-EnvironmentVariable.ps1 @@ -0,0 +1,36 @@ +function Set-EnvironmentVariable { +<# +.SYNOPSIS + Set or creates an environment variable. Must supply the store name, defaults to machine level. + +.PARAMETER Name + Sets or creates the environment variable with the specified name. + +.PARAMETER Value + Sets the environment variable with the specified value. + +.PARAMETER StoreName + The appropriate store to put the variable in. Defaults to machine. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias("Key")] + [string]$Name, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Value, + [Parameter(Mandatory = $true)] + [Alias("Store")] + [Alias("Location")] + [ValidateSet("Process","User","Machine")] + [string]$StoreName + ) + + $logLead = (Get-LogLeadName) + + Write-Host "$logLead : Set environment variable [$Name] in $StoreName store" + + [System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::$StoreName) +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.ps1 new file mode 100644 index 0000000..0868f75 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.ps1 @@ -0,0 +1,105 @@ +function Set-HelmDeploymentVersions { + <# +.SYNOPSIS + Updates the helm values file of a given environment to reflect the tags of the provided microservice names/versions. + +.PARAMETER RepoPath + The path to the gitops repository containing the helm environment definitions. + +.PARAMETER EnvironmentName + The name of the environment to update. + +.PARAMETER Packages + The list of microservice name/versions to update into the values file. + +.EXAMPLE + # This updates the version tags of microservices for Red14 + + $repoPath = ".\alkami.gitops.kubernetes" + $envName = "red14" + $packages = Format-ParseChocoPackages -Delimiter " " -Text @" + alkami.microservices.iaccountservicecontract.netcore 1.2.3 + alkami.services.tde-datafeed 0.0.9000 +"@ + + Set-HelmDeploymentVersions -RepoPath $repoPath -EnvironmentName $envName -Packages $packages -Verbose +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RepoPath, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$EnvironmentName, + [Parameter(Mandatory = $false)] + [object[]]$Packages + ) + + $logLead = Get-LogLeadName + + if (Test-IsCollectionNullOrEmpty -Collection $Packages) { + Write-Warning "$logLead : No packages to update. Exiting." + return + } + + # Fetch the values filepath/yaml for the environment. + # A null response here is presumably valid. The parent will call Write-Error so EAP needs to be Stop if you want it to break + # The parent will TC BuildProblem if there are issues reading things + $appYamls = Get-HelmApplicationYamls -RepoPath $RepoPath -EnvironmentName $EnvironmentName + + $values = $appYamls.values + $valuesPath = $appYamls.valuesPath + # $appName = $appYamls.applicationName + + # Create a mapping from image packageId to subchart name so we can avoid a O(n^2) lookup. + # Package names provided to this function will be based on the package id in the image repository, not the name of the subchart. + $imageToSubchartLookup = @{} + [array]$microserviceKeys = $values.Keys | Where-Object { $_ -like "alk-svc-*" } + foreach ($microservice in $microserviceKeys) { + $image = $values[$microservice].image.repository.split("/")[-1] + $imageToSubchartLookup[$image] = $microservice + } + + # Update the image tags if they have changes. + $dirty = $false + foreach ($package in $Packages) { + $packageName = $package.Name + + # TODO: Do we need a better descriptive name here for the block? + $blockName = "$packageName" + + Write-Host "##teamcity[blockOpened name='$blockName']" + Write-Host "$logLead : Packagename - $packageName" + $subchartName = $imageToSubchartLookup[$packageName] + + if ($null -eq $subchartName) { + # While we don't normally do TC messages in Module files, this seems appropriate per conversations and use-case + $errorMessage = "Could not find subchart definition for microservice `"$packageName`". Does the microservice exist in the $EnvironmentName values file?" + $identity = "SetHelm_$packageName" + Write-Host "##teamcity[buildProblem description='$errorMessage' identity='$identity']" + Write-Error "$logLead : $errorMessage" + # Need to process all the packages before erroring so multiple can be fixed at one time? + continue + } + + ## TODO: Can .image ever be null? Breaks the set down below + $currentTag = $values[$subchartName].image.tag + if ($currentTag -ne $package.Version) { + Write-Host "$logLead : Updating $subchartName tag from [$currentTag] to [$($package.Version)]" + $values[$subchartName].image.tag = $package.Version + $dirty = $true + } + Write-Host "##teamcity[blockClosed name='$blockName']" + } + + # Save the modified values file. + if ($dirty) { + Write-Host "$logLead : Updating image tags in values file for $EnvironmentName" + ConvertTo-Yaml -Data $values -OutFile $valuesPath -Force + return $valuesPath + } else { + Write-Host "$logLead : Environment values file for $EnvironmentName requires no updates. Exiting." + return $null + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.tests.ps1 new file mode 100644 index 0000000..56f6c7a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-HelmDeploymentVersions.tests.ps1 @@ -0,0 +1,26 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-HelmDeploymentVersions" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Test-IsCollectionNullOrEmpty -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-HelmApplicationYamls -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName ConvertTo-Yaml -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + Context "It does not error when Packages is null" { + It "Does not throw" { + { Set-HelmDeploymentVersions -RepoPath "TestDrive:\" -EnvironmentName "pester" } | Should Not Throw + } + It "Returns an empty result set" { + Set-HelmDeploymentVersions -RepoPath "TestDrive:\" -EnvironmentName "pester" | Should BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.ps1 new file mode 100644 index 0000000..5083337 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.ps1 @@ -0,0 +1,47 @@ +function Set-NewRelicAppName { +<# +.SYNOPSIS + Set enviroment to NewRelic.AppName in web.config +.PARAMETER enviroment + + The environment string to use in the app name. For example, "Staging Lane X" will result in a app name in "Staging Lane X NagConfiguration" +.PARAMETER configFiles + + An array of configuration files to update. Either this parameter or filePath must be provided +.PARAMETER filePath + + The parent path of configuration files. When specified without configFiles, will search for the files to update. Either this parameter + or configFiles must be provided +#> + [CmdletBinding()] + ## TODO: cbrand ~ $enviroment is misspelled. + param( + [Parameter(Mandatory = $true)][string]$enviroment, + [string[]]$configFiles, + [string]$filePath + ) + + $logLead = (Get-LogLeadName) + + if (Test-IsCollectionNullOrEmpty $configFiles) + { + if (!([String]::IsNullOrEmpty($filePath))) + { + Write-Host ("$logLead : Looking for Configuration Files in {0}" -f $filePath) + $configFiles = @() + $configFiles += Get-ConfigurationFiles -stagedFilePath $filePath -findTempFiles $true + $configFiles += Get-ConfigurationFiles -stagedFilePath $filePath -findTempFiles $false + } + else + { + ## TODO: Make this a parameter set to ensure these values are always passed + Write-Warning "$logLead : No Configuration Files Passed and No Filepath Provided. Execution cannot continue" + return + } + } + + foreach ($configFile in $configFiles) { + Set-NewRelicAppNameConfigFileValue -EnvironmentLabel $enviroment -ConfigFilePath $configFile + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.tests.ps1 new file mode 100644 index 0000000..eac4f00 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppName.tests.ps1 @@ -0,0 +1,98 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-NewRelicAppName + +Describe "Set-NewRelicAppName" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + $tempOrb = ($tempPath + "\ORB") + + if (!(Test-Path $tempOrb)) + { + New-Item -ItemType Directory $tempOrb | Out-Null + } + + Write-Warning ("Using temp path: $tempOrb for tests") + + # Make Directories and Files for Tests + $bankServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\BankService") + $radiumServiceDirectory = New-Item -ItemType Directory ($tempOrb + "\Radium") + + $bankFileRandomName = (Join-Path $bankServiceDirectory "foo.bar") + $radiumFileRandomName = (Join-Path $radiumServiceDirectory "foo.bar") + $bankFileNew = (Join-Path $bankServiceDirectory "new.web.config") + $radiumFileNew = (Join-Path $radiumServiceDirectory "new.Alkami.App.Radium.WindowsService.exe.config") + + $bankFile = (Join-Path $bankServiceDirectory "web.config") + $radiumFile = (Join-Path $radiumServiceDirectory "Alkami.App.Radium.WindowsService.exe.config") + + $testXml = @" + + + + + + + +"@ + + $testXml | Out-File $bankFile -Force + $testXml | Out-File $bankFileNew -Force + $testXml | Out-File $bankFileRandomName -Force + $testXml | Out-File $radiumFile -Force + $testXml | Out-File $radiumFileNew -Force + $testXml | Out-File $radiumFileRandomName -Force + + $testfiles = @($bankFileRandomName, $radiumFileRandomName) + + It "Updates the Application Name Using the File List Specified" { + + Set-NewRelicAppName "File List" -configFiles $testfiles + + (GC $bankFileRandomName | Select-String "File List Bank").Count | Should Be 1 + (GC $radiumFileRandomName | Select-String "File List Radium").Count | Should Be 1 + } + + It "Updates the Application Name Using the File Path Specified" { + + Set-NewRelicAppName "File Path" -filePath $tempOrb + + (GC $bankFile | Select-String "File Path Bank").Count | Should Be 1 + (GC $radiumFile | Select-String "File Path Radium").Count | Should Be 1 + (GC $bankFileNew | Select-String "File Path Bank").Count | Should Be 1 + (GC $radiumFileNew | Select-String "File Path Radium").Count | Should Be 1 + } + + It "Writes a Warning if an Invalid File is Specified" { + + $fakeFile = Join-Path $tempFile "someNonExistantFile.txt" + + if (Test-Path $fakeFile) + { + Remove-Item $fakeFile -Force + } + + { + ( Set-NewRelicAppName "Foo Bar" -configFiles @( $fakeFile ) 3>&1) -match "Could Not Find Config File $fakeFile" + } | Should Be $true + } + + It "Writes a Warning if No Files and No Path is Specified" { + + { + ( Set-NewRelicAppName "Foo Bar" 3>&1 ) -match "Execution cannot continue" + } | Should Be $true + } +} + +#endregion Set-NewRelicAppName \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppNameConfigFileValue.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppNameConfigFileValue.ps1 new file mode 100644 index 0000000..76250a9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-NewRelicAppNameConfigFileValue.ps1 @@ -0,0 +1,43 @@ +Function Set-NewRelicAppNameConfigFileValue { +<# +.SYNOPSIS + Set Environment Label to NewRelic.AppName in web.config + +.PARAMETER EnvironmentLabel + The environment string to use in the app name. For example, "Staging Lane X" will result in a app name in "Staging Lane X NagConfiguration" + +.PARAMETER ConfigFilePath + The parent path of configuration files. When specified without configFiles, will search for the files to update. + +.PARAMETER AppNameOverride + In rare cases the value should be overridden instead of looked up +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$EnvironmentLabel, + [string]$ConfigFilePath, + [string]$AppNameOverride + ) + + $logLead = (Get-LogLeadName) + + if (Test-Path $ConfigFilePath) { + + $folderName = (Get-ChildItem ($ConfigFilePath)).Directory.Name + + if (![string]::IsNullOrEmpty($AppNameOverride)) { + $folderName = $AppNameOverride + } + + Write-Verbose ("$logLead : Looking for Application Name Using Label {0}" -f $folderName) + $label = Get-NewRelicAppNameForConfigurationValue $folderName + Write-Verbose ("$logLead : Using Label {0}" -f $label) + + $appSettingValue = "$EnvironmentLabel $label" + Write-Verbose "$logLead : Setting [NewRelic.AppName] to [$appSettingValue]" + Set-AppSetting -key "NewRelic.AppName" -Value $appSettingValue -filePath $ConfigFilePath + } else { + Write-Error ("$logLead : Could Not Find Config File '{0}'" -f $ConfigFilePath) + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-ORBEnvironmentalVariables.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-ORBEnvironmentalVariables.ps1 new file mode 100644 index 0000000..e95601d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-ORBEnvironmentalVariables.ps1 @@ -0,0 +1,25 @@ +function Set-ORBEnvironmentalVariables { + <# +.SYNOPSIS + Sets an environment variable from interactive input +#> + [CmdletBinding()] + Param( + [PSObject]$EnvVariable + ) + + $logLead = Get-LogLeadName + $newValue = Read-Host ("Enter a value for {0}" -f $EnvVariable.Name) + + if (!([String]::IsNullOrEmpty($EnvVariable.ValidationRegex))) { + Write-Verbose ("$logLead : Validating Input {0} with Regex {1}" -f $newValue, $EnvVariable.ValidationRegex) + if ([String]::IsNullOrEmpty($newValue) -or $newValue -notmatch $envVariable.ValidationRegex) { + Write-Warning ($EnvVariable.ValidationError) + Set-ORBEnvironmentalVariables $EnvVariable + } else { + Write-Host ("$logLead : Setting machine environmental variable {0} to {1}" -f $EnvVariable.Name, $newValue) + [Environment]::SetEnvironmentVariable($EnvVariable.Name, $newValue, "Machine") + } + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-RedisConnectionString.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-RedisConnectionString.ps1 new file mode 100644 index 0000000..687e256 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-RedisConnectionString.ps1 @@ -0,0 +1,102 @@ +function Set-RedisConnectionString{ +<# +.SYNOPSIS +Sets the redis connection string in the machine config, and removes +the legacy app setting redis connection. + +.DESCRIPTION +Overwrites any redis connection strings in configuration.connectionstrings +Removes legacy redis connections located in configuration.appsettings + +.PARAMETER RedisFQDN +String, the full qualified domain name of the redis server. + +.PARAMETER Port +The port for the redis server. + +.PARAMETER Password +String, the password for the redis server. + +.PARAMETER use64bit +Boolean, passed to Get-DotNetConfigPath - if set to true, will return the filepath +of the 64 bit machine config file. + +.EXAMPLE +Set-RedisConnectionString -RedisFQDN "redis-19226.redisstg-gen4.fh.local" -Password "P@ssw0rd" -Port 19226 +Base usage, will set the redis connection string for this machine to + +and remove the legacy connection located in app settings. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True)] + [Alias("ComputerName","Server")] + [string]$RedisFQDN, + [Parameter(Mandatory=$True)] + $Port, + [Parameter(Mandatory=$False)] + [string]$Password, + [Parameter(Mandatory=$False)] + [bool]$use64Bit = $True, + [Parameter(Mandatory=$False)] + [bool]$useSSL = $False + ) + begin{ + $logLead = (Get-LogLeadName); + + Write-Verbose "$logLead Getting Maching.Config path, use64Bit set to $use64Bit" + $machineConfigPath = Get-DotNetConfigPath -use64Bit $use64Bit + if(!$machineConfigPath){throw "Machine config path could not be found"} + + Write-Verbose "$logLead Reading machine config file located at $machineConfigPath" + $machineConfig = Read-XMLFile $machineConfigPath + if(!$machineConfig){throw "Machine config at $machineConfigPath could not be converted to xml"} + + Write-Verbose "$logLead Constructing redis xml string" + if($Password){$PasswordString = ",password=$Password"} + + $sslString = ""; + if($useSSL) + { + $sslString = ",ssl=true" + } + + $RedisXmlString = '' + $RedisXmlString = $RedisXmlString -f $RedisFQDN,$Port,$PasswordString,$sslString + } + process{ + Write-Verbose "$logLead Ensuring configuration and connection string nodes exist" + if(!$machineConfig.configuration){ + [void]$machineConfig.AppendChild($machineConfig.CreateNode("element","configuration", $null)) + } + if(!$machineConfig.configuration.connectionStrings){ + [void]$machineConfig.SelectSingleNode("configuration").AppendChild($machineConfig.CreateElement("connectionStrings")) + } + + Write-Verbose "$logLead Removing current redis string" + $connectionStrings = $machineConfig.configuration.SelectSingleNode("connectionStrings") + if($connectionStrings.SelectNodes("add").count -gt 0){ + $CurrentRedisSettings = $connectionStrings.add | Where-Object {$_.Name -eq "RedisSetting"} + $CurrentRedisSettings | ForEach-Object {[void]$connectionStrings.RemoveChild($_)} + } + + Write-Verbose "$logLead Setting new redis string" + $RedisXmlDoc = [xml]($RedisXmlString) + $RedisNode = $machineConfig.ImportNode($RedisXmlDoc.FirstChild, $true) + [void]$connectionStrings.AppendChild($RedisNode) + + + Write-Verbose "$logLead Removing legacy redis string, if exists" + $appSettings = $machineConfig.configuration.appSettings + if($appSettings){ + if($appSettings.SelectNodes("add").count -gt 0){ + $LegacyRedisSettings = $appSettings.add | Where-Object {$_.key -eq "RedisHostCommaSeperatedEndpointsWithPorts"} + $LegacyRedisSettings | ForEach-Object { [void]$appSettings.RemoveChild($_)} + } + } + + Write-Verbose "$logLead Saving machine config to path $machineConfigPath" + $machineConfig.Save($machineConfigPath) + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-RegistryKeyValue.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-RegistryKeyValue.ps1 new file mode 100644 index 0000000..f91028c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-RegistryKeyValue.ps1 @@ -0,0 +1,37 @@ +function Set-RegistryKeyValue { +<# +.SYNOPSIS + Set a registry key value + +.DESCRIPTION + Create or sets registry key value given a path, type and value. + +.EXAMPLE + Set-RegistryKeyValue -regKey HKCU:\Environment\foo\bar -regkeyValueData '666' -regKeyValueType DWORD -Verbose +#> + [cmdletbinding()] + param ( + $regKey, + $regkeyValueData, + $regKeyValueType + ) + $logLead = (Get-LogLeadName) + $regKeyName = Split-Path $regKey -Parent + $regKeyValue = Split-Path $regKey -Leaf + + if (Test-RegistryKey $regKeyName) { + Write-Verbose "$logLead : Creating Registry Key $regKey" + $regKeyResult = New-ItemProperty -Path $regKeyName -Name $regKeyValue -Value $regkeyValueData -PropertyType $regKeyValueType -Force + Write-Verbose "$logLead : Registry key value set to $regKey." + } else { + throw "$logLead : Registry key ($regKeyName) does not exist." + } + + if ($regKeyResult.$regKeyValue -eq $regKeyValueData) { + Write-Verbose "$logLead : Successfully set registry value to $regKeyValueData." + return $true + } else { + Write-Warning "$logLead : Failed to set registry value." + return $false + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-SMSvcHostSids.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-SMSvcHostSids.ps1 new file mode 100644 index 0000000..2131e3f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-SMSvcHostSids.ps1 @@ -0,0 +1,128 @@ +function Set-SMSvcHostSids { +<# +.SYNOPSIS + Checks to see if the SMSvcHost.exe.config contains permissions for SIDs for the DBMS and Micro accounts. Adds them and the containing xml nodes if not. +#> + [CmdletBinding()] + param( + ) + + $logLead = (Get-LogLeadName); + + $configFilePath = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SMSvcHost.exe.config" + $dbmsAccountName = Get-AppSetting "DatabaseMicroServiceAccount" + $dbmsSid = $null + + if($null -ne $dbmsAccountName) + { + $dbmsSid = Get-SIDFromUserName $dbmsAccountName + } + else { + throw ("$logLead : Machine.config must have an appsetting value for the dbms account.") + } + + $microAccountName = Get-AppSetting "NonDatabaseMicroServiceAccount" + $microSid = $null + + if($null -ne $microAccountName) + { + $microSid = Get-SIDFromUserName $microAccountName + } + else { + throw ("$logLead : Machine.config must have an appsetting value for the micro account.") + } + + try { + [Xml]$smSvcHostConfig = Read-XMLFile $configFilePath -ErrorAction SilentlyContinue + if ([String]::IsNullOrEmpty($smSvcHostConfig)) { + throw ("$logLead : The SMSvcHost.exe.config config file could not be found") + } + } + catch { + throw ("$logLead : The SMSvcHost.exe.config config file could not be read or is invalid.") + } + + $configFileIsDirty = $false; + + $xmlBlock = [xml]" + + + + + + + " + + $xmlNodeNames = @( + "system.serviceModel.activation", + "net.tcp", + "allowAccounts", + "add" + ) + + $nullNodeName = $null; + $knownParentNode = "configuration"; + foreach ($xmlNodeName in $xmlNodeNames) { + if($smSvcHostConfig.SelectNodes("//$xmlNodeName").Count -eq 0) + { + $nullNodeName = $xmlNodeName + break; + } + else { + $knownParentNode = $xmlNodeName + } + } + + if(($null -eq $nullNodeName) -or ($nullNodeName -eq "add")) + { + $dbmsNode = $smSvcHostConfig.SelectNodes("//add[@securityIdentifier='$dbmsSid']") + # test for existing sids + # just 2 potential SIDs here, so not bothering with complex loop logic. + if($null -eq $dbmsNode -or $dbmsNode.Count -eq 0 ) + { + Write-Verbose ("$logLead : Dbms SID does not exist. New SIDs being added.") + $configFileIsDirty = $true; + + $newNode = [xml]""; + $smSvcHostConfig.SelectSingleNode("//allowAccounts").AppendChild($smSvcHostConfig.ImportNode($newNode.add, $true)) + } + + $microNode = $smSvcHostConfig.SelectNodes("//add[@securityIdentifier='$microSid']") + if($null -eq $microNode -or $microNode.Count -eq 0 ) + { + Write-Verbose ("$logLead : Micro SID does not exist. New SIDs being added.") + $configFileIsDirty = $true; + + $newNode = [xml]""; + $smSvcHostConfig.SelectSingleNode("//allowAccounts").AppendChild($smSvcHostConfig.ImportNode($newNode.add, $true)) + } + } + else { + Write-Verbose ("$logLead : $nullNodeName does not exist. New SIDs being added.") + $configFileIsDirty = $true + + # This could be one line, but it's far more readable split out like this. + $newNodes = $xmlBlock.SelectSingleNode("//$nullNodeName"); + $importedNodes = $smSvcHostConfig.ImportNode($newNodes, $true); + $smSvcHostConfig.SelectSingleNode("//$knownParentNode").AppendChild($importedNodes) + } + + if ($configFileIsDirty) { + Write-Host ("$logLead : Saving the Modified The SMSvcHost.exe.config Configuration File" -f $nrConfigPath) + + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + + try { + Save-XMLFile $configFilePath $smSvcHostConfig.OuterXml.Replace('xmlns="" ', [String]::Empty) $utfNoBOM + } + catch { + $ErrorMessage = $_.Exception.Message + Write-Warning ("$logLead : Saving file failed.") + Write-Warning ("$logLead : $ErrorMessage") + return + } + } + else { + Write-Host "$logLead : No Changes Required to The SMSvcHost.exe.config" + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-ServerRoleEnvironmentalVariable.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-ServerRoleEnvironmentalVariable.ps1 new file mode 100644 index 0000000..25f320e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-ServerRoleEnvironmentalVariable.ps1 @@ -0,0 +1,19 @@ +function Set-ServerRoleEnvironmentalVariable { +<# +.SYNOPSIS + Sets the Server Role Environmental Variable + +.PARAMETER Role + Accepted values are Web, Microservice, App, Fabric +#> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet("Web","Microservice","App","Fabric")] + [string]$Role + ) + + return (Set-EnvironmentVariable -Name "ServerRole" -Value $Role -StoreName Machine) +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.ps1 new file mode 100644 index 0000000..31fb5b0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.ps1 @@ -0,0 +1,180 @@ +function Set-StaticConfigValues { +<# +.SYNOPSIS + Sets Static Values We Always Want in Various App / Web Configuration Files. Removes WCF tracing nodes and + copies existing app settings from the originally deployed config file to the temporary configuration file +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string[]]$temporaryConfigFiles, + [Parameter(Mandatory=$false)] + [String]$stagedFilePath = "C:\temp\Deploy\Orb\", + [Parameter(Mandatory=$false)] + [String]$deployedFilePath + ) + + $logLead = (Get-LogLeadName); + + if([string]::IsNullOrEmpty($deployedFilePath)) { + $deployedFilePath = (Get-OrbPath) + } + + if ($stagedFilePath.EndsWith("\") -and !($deployedFilePath.EndsWith("\"))) { + $deployedFilePath = $deployedFilePath + "\" + } + + # We will always set these values if the node is there + $staticNodeValues = @( + @{XPath = "//appSettings/add[@key='ShouldDisplayUncaughtExceptions']"; AttributeName = "value"; AttributeValue = "false" }, + @{XPath = "//appSettings/add[@key='ForceUseOfProductionAssetsInDebug']"; AttributeName = "value"; AttributeValue = "true"}, + @{XPath = "//system.web/compilation[@debug]"; AttributeName = "debug"; AttributeValue = "false" }, + @{XPath = "//httpErrors"; AttributeName = "errorMode"; AttributeValue = "DetailedLocalOnly"}, + @{XPath = "//authentication/forms[@cookieless]"; AttributeName = "cookieless"; AttributeValue = "UseCookies"}, + @{XPath = "//glimpse[@defaultRuntimePolicy]"; AttributeName = "defaultRuntimePolicy"; AttributeValue = "Off"}, + @{XPath = "//customErrors"; AttributeName = "mode"; AttributeValue = "RemoteOnly"} + ) + + # We will always bring these appSetting values over if they previously existed, even if they are not + # in the new configuration file + $appSettingsToKeep = @( + + "IsTransactionCachingEnabled" + ) + + foreach ($configFile in $temporaryConfigFiles) + { + Write-Host ("$logLead : Updating Configuration Files '{0}'" -f $configFile) + + # Read the temp config file as XML + [Xml]$configXml = (Get-Content $configFile) + + # Remove system.diagnostics child nodes to disable WCF tracing + $diagnosticNodes = $configXml.SelectNodes("//system.diagnostics") + Write-Host ("$logLead : Removing system.diagnostics Child Nodes") + if ($null -ne $diagnosticNodes -and $diagnosticNodes.Count -gt 0 ) { + + if (($diagnosticNodes | Select-Object -First 1).HasChildNodes) + { + $diagnosticNodes.ChildNodes | ForEach-Object { + + $diagnosticNodes.RemoveChild($_) | Out-Null + } + } + else + { + Write-Host ("$logLead : system.diagnostics Node Has No Children") + } + } + + # This is the file we'll save the new.*.config as when we're done tweaking it + [Regex]$newMatchRegex = "\\new\." + $deployedConfigFileName = $newMatchRegex.Replace($configFile, "\") + + # Copy other Configuration from the Originally Deployed Config File to the New One + # We use $deployedConfigFileName as the deployed config won't be new.web.config, for example, it'll just be web.config + # We also replace the staging path with the deployment path. So if we're editing C:\temp\deploy\BankService\new.web.config, + # this value might be replaced as C:\ORB\BankService\web.config + $originalConfigFilename = $deployedConfigFileName.Replace($stagedFilePath, $deployedFilePath) + + if (Test-Path $originalConfigFilename) + { + $nodesToCopy = ("//appSettings","//quartz") + Write-Verbose ("$logLead : Reading Original Config File '{0}'" -f $originalConfigFileName) + [Xml]$originalConfig = Get-Content $originalConfigFilename + + foreach ($node in $nodesToCopy) + { + Write-Verbose ("$logLead : Checking for Child Nodes of {0} to Copy from Deployed Config" -f $node) + + try { + $originalNode = $originalConfig.SelectSingleNode($node) + $tempNode = $configXml.SelectSingleNode($node) + } + catch { + # Do Nothing, We Want to Log the Circumstance Below Before Continuing + Write-Verbose "$logLead : Catch Block Hit When Searching for Node" + } + + if ($null -eq $originalNode -or $originalNode.Count -eq 0) + { + Write-Warning ("$logLead : Node {0} Not Found in Deployed Config File {1}" -f $node, $originalConfigFilename) + continue; + } + elseif (!($originalNode.HasChildNodes)) + { + Write-Warning ("$logLead : Node {0} Has No Children in Temporary Config File {1}" -f $node, $originalConfigFilename) + continue; + } + + if ($null -eq $tempNode -or $tempNode.Count -eq 0) + { + Write-Warning ("$logLead : Node {0} Not Found in Temporary Config File {1}" -f $node, $configFile) + continue; + } + elseif (!($tempNode.HasChildNodes)) + { + Write-Warning ("$logLead : Node {0} Has No Children in Temporary Config File {1}" -f $node, $configFile) + } + + foreach ($originalChildNode in $originalNode.ChildNodes | Where-Object {$_.NodeType -ne "Comment"}) + { + Write-Verbose ("$logLead : Looking for Key {0} in Temporary Config File {1}" -f $originalChildNode.Key, $configFile) + [array]$newKey = $tempNode.ChildNodes | Where-Object {$_.NodeType -ne "Comment" -and $_.Key.ToString() -eq $originalchildNode.Key.ToString()} + + if (($null -eq $newKey -or $newKey.Count -eq 0) -and $appSettingsToKeep.Contains($originalChildNode.Key)) + { + # We need to add the appSetting + $keyToUpdate = $newKey | Select-Object -First 1 + Write-Host ("$logLead : Adding New App Setting with Key '{0}' and Value '{1}'" -f $originalChildNode.Key, $originalChildNode.Value) + + $tempDoc = New-Object System.Xml.XmlDocument + $tempDoc.LoadXml(('' -f $originalChildNode.Key, $originalChildNode.Value)) + $newSettingNode = $configXml.ImportNode($tempDoc.DocumentElement, $true) + $tempNode.AppendChild($newSettingNode) | Out-Null + } + elseif ($null -ne $newKey -and $newKey.Count -gt 0) + { + $keyToUpdate = $newKey | Select-Object -First 1 + Write-Host ("$logLead : Setting Node '{0}' to value '{1}'. Original value was: '{2}'" -f $originalChildNode.Key, $originalChildNode.Value, $keyToUpdate.Value) + $keyToUpdate.Value = $originalChildNode.Value + } + else + { + Write-Host ("$logLead : Key {0} Not Present in the New File -- It Will be Dropped. Previous value was: {1}" -f $originalChildNode.Key, $originalChildNode.Value) + } + } + } + + # Special case - Copy system.webServer/Cors section if it exists (SRE-9726) + # Get setting from deployed and staged configs + $corsNodeDeployed = $originalConfig.SelectSingleNode("//system.webServer/cors") + $corsNodeStaged = $configXml.SelectSingleNode("//system.webServer/cors") + + # Only copy to staged config if it does not already exist + if (($null -ne $corsNodeDeployed) -and ($null -eq $corsNodeStaged)) { + Write-Host ("Cors section found in deployed config. Copying to staged config...") + $nodeStaged = $configXml.SelectSingleNode("//system.webServer") + $importNode = $nodeStaged.OwnerDocument.ImportNode($corsNodeDeployed, $true) + $nodeStaged.AppendChild($importNode) + } + } + else + { + Write-Warning ("$logLead : Expected config file at {0} but it doesn't exist. This may be a new file" -f $originalConfigFilename) + } + + # Set Static Values We Always Want in the Config + foreach ($node in $staticNodeValues) { + + Write-Host ("$logLead : Setting {0} to {1}" -f $node.XPath, $node.AttributeValue) + (Set-XmlNodeValue $configXml ` + -NodeString $node.XPath ` + -AttributeName $node.AttributeName ` + -AttributeValue $node.AttributeValue ` + -ErrorAction SilentlyContinue) | Out-Null + } + + Save-XMLFile $configFile $configXml.OuterXml $utfNoBOM + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.tests.ps1 new file mode 100644 index 0000000..a5b400f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Set-StaticConfigValues.tests.ps1 @@ -0,0 +1,194 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-StaticConfigValues + +Describe "Set-StaticConfigValues" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + + $orbPath = ($tempPath + "\ORB") + $stagePath = ($tempPath + "\Stage") + + if (!(Test-Path $orbPath)) + { + New-Item -ItemType Directory $orbPath | Out-Null + } + + if (!(Test-Path $stagePath)) + { + New-Item -ItemType Directory $stagePath | Out-Null + } + + Write-Warning ("Using temp path: $tempPath for tests") + + # Make Directories and Files for Tests + $bankServiceDirectoryORB = New-Item -ItemType Directory ($orbPath + "\BankService") + $bankServiceDirectoryTemp = New-Item -ItemType Directory ($stagePath + "\BankService") + + $bankFileNameORB = (Join-Path $bankServiceDirectoryORB "web.config") + $bankFileNameTemp = (Join-Path $bankServiceDirectoryTemp "new.web.config") + + # This is a bogus config file that covers everything we want to modify + # Intentionally set with all the wrong values + $utfNoBOM = New-Object System.Text.UTF8Encoding($false) + $sampleBankContent = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@ + + # Save our Bank File + Save-XMLFile $bankFileNameORB $sampleBankContent $utfNoBOM + + # Save out Temp File and Modify App Settings for Testing Purposes + $sampleBankContent | Out-File $bankFileNameTemp -Force + [xml]$xml = GC $bankFileNameTemp + + # Change some App Settings for Validation + Set-XmlNodeValue $xml "//appSettings/add[@key='FakeSetting']" "value" "WrongValue" + Set-XmlNodeValue $xml "//quartz/add[@key='quartz.scheduler.instanceName']" "value" "WrongValue" + + # Remove the Broadcasters Node for Validation + $asNode = $xml.SelectSingleNode("//appSettings") + $bcNode = $xml.SelectSingleNode("//appSettings/add[@key='Broadcasters']") + $asNode.RemoveChild($bcNode) + + $tcNode = $xml.SelectSingleNode("//appSettings/add[@key='IsTransactionCachingEnabled']") + $asNode.RemoveChild($tcNode) + + # Save the File + Save-XMLFile $bankFileNameTemp $xml.OuterXml $utfNoBOM + $tempXml = GC $bankFileNameTemp + + # Execute the Function and Read the Results as XML + Set-StaticConfigValues @( $bankFileNameORB, $bankFileNameTemp ) $bankServiceDirectoryTemp $bankServiceDirectoryORB -Verbose + [xml]$validation = GC $bankFileNameTemp + + Context "Setting Static Configuration Values" { + + It "Sets ShouldDisplayUncaughtExceptions to 'false'" { + + $validation.SelectSingleNode("//appSettings/add[@key='ShouldDisplayUncaughtExceptions']").Value | Should Be "false" + } + + It "Sets ForceUseOfProductionAssetsInDebug to 'true'" { + + $validation.SelectSingleNode("//appSettings/add[@key='ForceUseOfProductionAssetsInDebug']").Value | Should Be "true" + } + + It "Sets compilation.debug to 'false'" { + + $validation.SelectSingleNode("//system.web/compilation[@debug]").Debug | Should Be "false" + } + + It "Sets httpErrors.errorMode to 'DetailedLocalOnly'" { + + $validation.SelectSingleNode("//httpErrors").Debug + } + + It "Sets forms.cookieless to 'UseCookies'" { + + $validation.SelectSingleNode("//authentication/forms[@cookieless]").cookieless | Should Be "UseCookies" + } + + It "Sets glimpse.defaultRuntimePolicy to 'Off'" { + + $validation.SelectSingleNode("//glimpse[@defaultRuntimePolicy]").defaultRuntimePolicy | Should Be "Off" + } + + It "Sets customErrors.mode to 'RemoteOnly'" { + + $validation.SelectSingleNode("//customErrors").mode | Should Be "RemoteOnly" + } + } + + Context "Copying Child Nodes of Config Sections" { + + It "Copies appSettings Values from the deployed config to the new config" { + + $validation.SelectSingleNode("//appSettings/add[@key='FakeSetting']").Value | Should Be "FakeValue" + } + + It "Copies quartz Values from from the deployed config to the new config" { + + $validation.SelectSingleNode("//quartz/add[@key='quartz.scheduler.instanceName']").Value | Should Be "FakeValue" + } + + It "Removes All Child Nodes from system.diagnostics to Disable WCF Tracing" { + + $validation.SelectSingleNode("//system.diagnostics").ChildNodes.Count | Should Be 0 + } + } + + Context "Creating Config Values Which Do Not Exist" { + + It "Creates the IsTransactionCachingEnabled Node with the Previous Value If it Doesn't Exist" { + + $validation.SelectSingleNode("//appSettings/add[@key='IsTransactionCachingEnabled']").Value | Should Be "false" + } + } +} + +#endregion Set-StaticConfigValues diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.ps1 new file mode 100644 index 0000000..d619692 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.ps1 @@ -0,0 +1,30 @@ +function Test-AlkamiApiComponentManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the ApiComponentManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of validity + +.PARAMETER ApiComponentManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$ApiComponentManifest + ) + + $success = $true + $resultMessages = @() + + if ([string]::IsNullOrWhiteSpace($ApiComponentManifest.targetApp)) { + $resultMessages += 'The ApiComponent manifest target App is not present. This is required and should be a valid Package name.' + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.tests.ps1 new file mode 100644 index 0000000..9be11cb --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiApiComponentManifest10.tests.ps1 @@ -0,0 +1,41 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiApiComponentManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiApiComponentManifest10 @{ + targetApp = 'Alkami.Fake.App' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiApiComponentManifest10 @{ + targetApp = '' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.ps1 new file mode 100644 index 0000000..f2c0566 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.ps1 @@ -0,0 +1,142 @@ +function Test-AlkamiFluentMigrationManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the FluentMigrationManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER FluentMigrationManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$FluentMigrationManifest + ) + + $success = $true + $resultMessages = @() + + $parsedRuntime = "" + + if ([string]::IsNullOrWhiteSpace($FluentMigrationManifest.runtime)) { + $resultMessages += "packageManifest/fluentMigrationManifest/runtime must be populated" + $success = $false + } else { + try { + $parsedRuntime = Get-ValidatedRuntimeParameter -Runtime $FluentMigrationManifest.runtime + } catch { + $resultMessages += "packageManifest/fluentMigrationManifest/runtime contains an invalid value. Valid values are [framework,dotnetcore]" + $success = $false + } + } + + #region handle version + $versionInvalid = $false + $version = $FluentMigrationManifest.version + try { + if ($null -eq [Version]$version) { + $versionInvalid = $true + } elseif (([Version]$version) -lt ([Version]'1.4.0')) { + $versionInvalid = $true + } + } catch { + $versionInvalid = $true + } + + if ($versionInvalid) { + if ($parsedRuntime -eq 'dotnetcore') { + $resultMessages += 'packageManifest/fluentMigrationManifest/version is not a valid Version value. This is required and should be a valid version.' + $success = $false + } else { + if ([string]::IsNullOrWhiteSpace($version)) { + # this is a valid scenario + # I still assume most people will provide a value + } else { + # Different message for framework than core + $resultMessages += 'packageManifest/fluentMigrationManifest/version is not a valid Version value. This is not required but if provided must be a valid version. It will default to 1.4.0 if not provided.' + $success = $false + } + } + } + #endregion handle version + + #region migrationtype + # We don't care if it's _empty_ because we will just ignore that value and default to Tenant + $migrationType = $FluentMigrationManifest.migrationType.'#text' + if (-not [string]::IsNullOrWhiteSpace($migrationType)) { + if (@('tenant','master','megapod') -notcontains $migrationType) { + $resultMessages += "packageManifest/fluentMigrationManifest/migrationType must be populated with a valid value when provided. Valid values are [tenant,master,megapod]. This will default to tenant if not provided." + $success = $false + } + } + + $online = $FluentMigrationManifest.migrationType.online + if (-not [string]::IsNullOrWhiteSpace($online)) { + if (@('true','false') -notcontains $online) { + $resultMessages += "packageManifest/fluentMigrationManifest/migrationType/online must be populated with a valid value when provided. Valid values are [true,false]. This will default to false if not provided." + $success = $false + } + } + #endregion migrationtype + + $serverType = $FluentMigrationManifest.serverType + if (-not [string]::IsNullOrWhiteSpace($serverType)) { + # An array so as we add new types we support, we can just add here + if (@('MSSQL') -notcontains $serverType) { + $resultMessages += "packageManifest/fluentMigrationManifest/serverType must be populated with a valid value when provided. Valid values are [MSSQL]. This will default to MSSQL if not provided." + $success = $false + } + } + + if ([string]::IsNullOrWhiteSpace($FluentMigrationManifest.db_role)) { + $resultMessages += "packageManifest/fluentMigrationManifest/db_role must be populated" + $success = $false + } + + #region handle entryPoint +<# +Consider this: + + valid value + + +All because of some copy-pasta things. +I need the second entry to puke. Test-IsCollectionNullOrEmpty won't puke on this. +#> + + $foundSomeAssemblies = $false + $assemblyErrors = @() + # It is valid to put more than one assembly in the file + # It is also discouraged, but there are a few situations that exist in May 2022 that already could require this + # Specifically around ORB migrations with Content and Tenant database files. + # Does that get used here today? No. Am I planning for similar scenarios because I know what is coming? Yes. - cbrand + foreach ($assemblyName in $FluentMigrationManifest.entryPoint) { + # An entryPoint in FluentMigrator is a dll. So it's an assembly. + # EntryPoint is the moving-to common name for all projects, not just .net stuff + $foundSomeAssemblies = $true + if ([string]::IsNullOrWhiteSpace($assemblyName)) { + $assemblyErrors += "Found an empty entryPoint value in the manifest" + } + } + + if (-not $foundSomeAssemblies) { + # NO assembly entries found + $resultMessages += "packageManifest/fluentMigrationManifest must have one or more entryPoint specified" + $success = $false + } else { + # Found some assembly points + if ($assemblyErrors) { + # but there were errors (blanks) + $resultMessages += ("packageManifest/fluentMigrationManifest $assemblyErrors") + $success = $false + } + } + #endregion handle entryPoint + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.tests.ps1 new file mode 100644 index 0000000..3a4f905 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiFluentMigrationManifest10.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiFluentMigrationManifest10" { + Context "Test valid manifest - Core" { + $result = Test-AlkamiFluentMigrationManifest10 @{ + runtime = 'core' + db_role = 'some name' + entryPoint = @( + 'just an example.dll' + ) + version = '3.3.1' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test valid manifest - Framework" { + $result = Test-AlkamiFluentMigrationManifest10 @{ + runtime = 'framework' + db_role = 'some name' + entryPoint = @( + 'just an example.dll' + ) + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiFluentMigrationManifest10 @{ + runtime = 'invalid' + serverType = 'nonsense' + migrationType = @{ + '#text' = '' + online = 'garbage' + } + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least two warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 2 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.ps1 new file mode 100644 index 0000000..e9916fd --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.ps1 @@ -0,0 +1,54 @@ +function Test-AlkamiHotfixManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the HotfixManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER HotfixManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$HotfixManifest + ) + + $success = $true + $resultMessages = @() + + if (Test-StringIsNullOrWhiteSpace -Value $HotfixManifest.fixedInOrbVersion) { + $resultMessages += 'The hotfix manifest target Version is missing. This is required and should be a valid version.' + $success = $false + } else { + try { + # Either this is a valid - and castable - version string, or it throws + [Version]$HotfixManifest.fixedInOrbVersion | Out-Null + } catch { + $resultMessages += 'The hotfix manifest target Version is not a valid Version object. This is required and should be a valid version.' + $success = $false + } + } + + + $serverTier = $HotfixManifest.serverTier + $validHotfixServerTiers = Get-ValidHotfixServerTiers + $validHotfixServerTiersString = $validHotfixServerTiers -join "," + + if (Test-StringIsNullOrWhiteSpace -Value $serverTier) { + $resultMessages += "The hotfix manifest serverTier is missing. This is required and must be one of these values: $validHotfixServerTiersString" + # TODO: Will need to be revisited after 2022.4 release in the future (see also Get-PackageMetadataV2) + # Original 1.0 manifests did not have serverTier + # Null-or-Whitespace will mean Install-to-All-tiers until 2022.4 when hotfixes have serverTier node + #$success = $false + } elseif ($serverTier -notin $validHotfixServerTiers) { + $resultMessages += "The hotfix manifest serverTier is not a valid value. It must be one of these values: $validHotFixServerTiersString" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.tests.ps1 new file mode 100644 index 0000000..38e42e2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiHotfixManifest10.tests.ps1 @@ -0,0 +1,172 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiHotfixManifest10" { + Mock -CommandName Get-ValidHotfixServerTiers -ModuleName $moduleForMock -MockWith { + return @("VALID", "TIER", "LIST") + } + # Default to mocking all strings returning as null/whitespace + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $true + } + + Context "Test valid manifest" { + # This context is for valid manifests, therefore, any invocation of + # Test-StringIsNullOrWhiteSpace will return $false + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $false + } + $result = Test-AlkamiHotfixManifest10 @{ + fixedInOrbVersion = '2000.0.0' + serverTier = 'TIER' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test manifest with INVALID version" { + Mock -CommandName Get-ValidHotfixServerTiers -ModuleName $moduleForMock -MockWith { + return @("VALID", "TIER", "LIST") + } + # Both members have values, but one is INVALID + # Therefore both calls to + # Test-StringIsNullOrWhiteSpace will return $false + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $false + } + $invalidVersionResult = Test-AlkamiHotfixManifest10 @{ + fixedInOrbVersion = 'REPLACEME' + serverTier = 'TIER' + } + + It "Returns success false" { + $invalidVersionResult.success | Should -BeFalse + } + + It "Actually produced warnings" { + $invalidVersionResult.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $invalidVersionResult.results.Count | Should -BeGreaterOrEqual 1 + } + + It "Has messages about target version" { + $invalidVersionResult.results | Should -Match "target version is not a valid" + } + } + + Context "Test manifest with MISSING version" { + Mock -CommandName Get-ValidHotfixServerTiers -ModuleName $moduleForMock -MockWith { + return @("VALID", "TIER", "LIST") + } + + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $true + } -ParameterFilter { $Value -ne 'TIER' } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $false + } -ParameterFilter { $Value -eq 'TIER' } + + $invalidVersionResult = Test-AlkamiHotfixManifest10 @{ + fixedInOrbVersion = '' + serverTier = 'TIER' + } + + It "Returns success false" { + $invalidVersionResult.success | Should -BeFalse + } + + It "Actually produced warnings" { + $invalidVersionResult.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $invalidVersionResult.results.Count | Should -BeGreaterOrEqual 1 + } + + It "Has messages about target version" { + $invalidVersionResult.results | Should -Match "target version is missing" + } + } + + Context "Test manifest with INVALID serverTier" { + Mock -CommandName Get-ValidHotfixServerTiers -ModuleName $moduleForMock -MockWith { + return @("VALID", "TIER", "LIST") + } + # Both members have values, but one is INVALID + # Therefore both calls to + # Test-StringIsNullOrWhiteSpace will return $false + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $false + } + $invalidServerTierResult = Test-AlkamiHotfixManifest10 @{ + fixedInOrbVersion = '2000.0.0' + serverTier = 'REPLACESERVERTIER' + } + + It "Returns success false" { + $invalidServerTierResult.success | Should -BeFalse + } + + It "Actually produced warnings" { + $invalidServerTierResult.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $invalidServerTierResult.results.Count | Should -BeGreaterOrEqual 1 + } + + It "Has messages about serverTier" { + $invalidServerTierResult.results | Should -Match "serverTier is not a valid" + } + } + Context "Test manifest with MISSING serverTier" { + Mock -CommandName Get-ValidHotfixServerTiers -ModuleName $moduleForMock -MockWith { + return @("VALID", "TIER", "LIST") + } + + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $true + } -ParameterFilter { $Value -ne '2000.0.0' } + Mock -CommandName Test-StringIsNullOrWhiteSpace -ModuleName $moduleForMock -MockWith { + return $false + } -ParameterFilter { $Value -eq '2000.0.0' } + + $invalidServerTierResult = Test-AlkamiHotfixManifest10 @{ + fixedInOrbVersion = '2000.0.0' + serverTier = '' + } + + # TODO: Will need to be revisited after 2022.4 release in the future (see also Get-PackageMetadataV2) + # Original 1.0 manifests did not have serverTier + # Null-or-Whitespace will mean Install-to-All-tiers until 2022.4 when hotfixes have serverTier node + It "Returns success TRUE until 2022.4 release" { + $invalidServerTierResult.success | Should -BeTrue -Because "Hotfix manifest v1.0 did not originally have serverTier nodes, so early hotfixes returned empty-string for serverTier" + } + + It "Actually produced warnings" { + $invalidServerTierResult.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $invalidServerTierResult.results.Count | Should -BeGreaterOrEqual 1 + } + + It "Has messages about serverTier" { + $invalidServerTierResult.results | Should -Match "serverTier is missing" + } + } + +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.ps1 new file mode 100644 index 0000000..5e8f59d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.ps1 @@ -0,0 +1,28 @@ +function Test-AlkamiInstallerManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the LegacyUtilityManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER InstallerManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$InstallerManifest + ) + + $success = $true + $resultMessages = @() + + # Installer manifests don't have testable structure that we care about at this time. + # This is ok. + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.tests.ps1 new file mode 100644 index 0000000..f115949 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiInstallerManifest10.tests.ps1 @@ -0,0 +1,23 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiInstallerManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiInstallerManifest10 @{ + anything = "is unused here" + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.ps1 new file mode 100644 index 0000000..6fb31fa --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.ps1 @@ -0,0 +1,30 @@ +function Test-AlkamiLegacyUtilityManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the LegacyUtilityManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER LegacyUtilityManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$LegacyUtilityManifest + ) + + $success = $true + $resultMessages = @() + + if (-not [string]::IsNullOrWhiteSpace($LegacyUtilityManifest.needsShared) -and -not (@('true','false') -contains $LegacyUtilityManifest.needsShared)) { + $resultMessages += "packageManifest/legacyUtilityManifest/needsShared must be true or false" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.tests.ps1 new file mode 100644 index 0000000..3dfdd49 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiLegacyUtilityManifest10.tests.ps1 @@ -0,0 +1,57 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiLegacyUtilityManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiLegacyUtilityManifest10 @{ + needsShared = $true + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test valid manifest" { + $result = Test-AlkamiLegacyUtilityManifest10 @{ + # This is valid and represents the lack of a value, but we have to give something to the object for parsing + # At runtime it will have a node element available and just won't process because the value doesn't have the child + needsShared = '' + } + + It "Tested successfully" { + $result | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiLegacyUtilityManifest10 @{ + needsShared = 'garbage' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.ps1 new file mode 100644 index 0000000..1e09ea3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.ps1 @@ -0,0 +1,170 @@ +function Test-AlkamiManifest { +<# +.SYNOPSIS + Test an AlkamiManifest.xml to ensure the file seems valid. + +.DESCRIPTION + Test and AlkamiManifest.xml to ensure the file seems valid. + This will return any of a number of errors, and a code. + The message should have all the remediation information you need to resolve the error. + The code should be supplied to an SDK/Tools team member for bug reporting or more detailed help. + +.PARAMETER Source + [string] The folder that has the AlkamiManifest.xml file to be validated. Alternately, the path to the file. Defaults to looking in the current folder. + +.PARAMETER TargetObject + [object] Used to bypass file loading. Most useful when combined with Get-PackageManifest -Path for ultimate power + +.INPUTS + An AlkamiManifest.xml file for evaluation + +.OUTPUTS + Either a successful message or a thrown error about what is broken. + +.EXAMPLE + Test-AlkamiManifest + +[Test-AlkamiManifest] : The file at C:\my\test\AlkamiManifest.xml appears to be valid for processing. + +.EXAMPLE + Test-AlkamiManifest C:\my\test\ + +[Test-AlkamiManifest] : The file at C:\my\test\AlkamiManifest.xml appears to be valid for processing. + +.EXAMPLE + Test-AlkamiManifest C:\my\test\AlkamiManifest.xml + +[Test-AlkamiManifest] : The file at C:\my\test\AlkamiManifest.xml appears to be valid for processing. + +#> + [CmdletBinding(DefaultParameterSetName = 'PassedPath')] + [OutputType([bool])] + Param( + [Parameter(Mandatory = $false, ParameterSetName = 'PassedPath')] + [ValidateNotNullOrEmpty()] + [ValidateScript( {Test-Path (Resolve-Path $_)})] + [Alias('Path', 'Folder', 'Target')] + [String]$Source = ".", + [Parameter(Mandatory = $true, ParameterSetName = 'PassedObject')] + [object]$TargetObject + ) + + $logLead = (Get-LogLeadName) + $resultMessages = @() + $success = $true + + $validateManifestNodes = @() + + #region parse/simplify parameterset + if ($PSCmdlet.ParameterSetName -eq 'PassedPath') { + $filename = (Get-AlkamiManifestFilename) + $targetPath = Resolve-Path $Source + if ((Split-Path $targetPath -Leaf) -ne $filename) { + $targetPath = (Join-Path $targetPath $filename) + } + if (-not (Test-Path $targetPath)) { + throw "$logLead : There is no file at $targetPath - Did you want to New-AlkamiManifest this file instead?" + } + $xmlContent = [Xml](Get-Content $targetPath) + if ($null -eq $xmlContent.packageManifest) { + throw "$logLead : There is no packageManifest tag in this file. It is invalid." + } + + $TargetObject = $xmlContent.packageManifest + } + #endregion parse/simplify parameterset + + #region parse/validate version + ## Add additional recognized version codes here. + $recognizedVersionCodes = @('1.0') + + if (-not $TargetObject.version) { + $resultMessages += 'There is no version tag in this file. It is invalid.' + $success = $false + } else { + $version = $TargetObject.Version -replace '\.','' + } + + if (-not ($recognizedVersionCodes -contains $TargetObject.version)) { + $resultMessages += "The version tag in this file is unrecognized or unsupported. It is invalid. Valid values are [$($recognizedComponentTypes -join ',')]" + $success = $false + } + #endregion parse/validate version + + # If we have gotten this far, we recognize the version and can test this manifest set + if ($success) { + #region parse/validate general + if ($null -eq $TargetObject.general) { + $resultMessages += 'There is no packageManifest/general tag in this file. It is invalid.' + $success = $false + } else { + $validateManifestNodes += "General" + } + + $recognizedComponentTypes = @( + 'Widget' + 'Service' + 'FluentMigration' + 'WebApplication' + 'WebExtension' + 'Repository' + 'Provider' + 'WebSite' + 'Installer' + 'SREModule' + 'LegacyUtility' + 'Hotfix' + 'ApiComponent' + ) + + $componentType = $TargetObject.general.componentType + + if ([string]::IsNullOrWhiteSpace($componentType)) { + $resultMessages += 'The packageManifest/general/componentType tag in this file is missing or empty. It is invalid.' + $success = $false + } else { + if (-not ($recognizedComponentTypes -contains $componentType)) { + $resultMessages += "The packageManifest/general/componentType tag in this file is unrecognized or unsupported. Valid values are $($recognizedComponentTypes -join ',')" + $success = $false + } else { + if ($componentType) { + $validateManifestNodes += "$($componentType)Manifest" + } + } + } + #endregion parse/validate general + + foreach ($manifestNode in $validateManifestNodes) { + $manifestFunctionName = $manifestNode + if ($manifestNode -eq 'General') { + # This is due to function name grouping so that they all appear together + $manifestFunctionName = 'ManifestGeneral' + } + if ($manifestNode -eq "SREModuleManifest") { + # This is due to Cole making a mistake in naming things + $manifestFunctionName = 'SREModuleManifest' + $manifestNode = 'moduleManifest' + } + if ($null -eq $TargetObject.$manifestNode) { + $resultMessages += "The expected manifest type subnode [$manifestNode] was not found" + $success = $false + } + + # Create a simplified scriptblock that takes a param and executes a built-string against the provided object + $dynamicString = "param (`$manifestObject) Test-Alkami$manifestFunctionName$version `$manifestObject" + $sb = [scriptblock]::Create($dynamicString) + $results = Invoke-Command -ScriptBlock $sb -ArgumentList @( $TargetObject.$manifestNode ) + + $resultMessages += @($results.results) + $success = $success -and $results.Success + } + } + + foreach($resultMessage in $resultMessages) { + if (-not [string]::IsNullOrWhiteSpace($resultMessage)) { + Write-Warning $resultMessage + } + } + + return $success +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.tests.ps1 new file mode 100644 index 0000000..974f26a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifest.tests.ps1 @@ -0,0 +1,166 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiManifest" { + function New-FakeServiceManifest { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-FakeServiceManifestWithReleaseManagement { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + true + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-InvalidServiceManifestWithReleaseManagement { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + not_a_bool + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-ValidFluentMigrationManifest { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.Migrations.UserReporting.Migrations + FluentMigration + + + framework + 1.4.0 + tenant + MSSQL + Alkami.Migrations.UserReporting.Migrations.dll + Alkami.Migrations.UserReporting_Service + + +"@ + $testFile = "TestDrive:\NewFluentMigrationAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "SUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiManifestFilename -MockWith { return "NewServiceAlkamiManifest.xml" } + + # The expectation is that we will have multiple versions here. We might need more contexts per version at that point. + Context "When Testing a Service Manifest" { + New-FakeServiceManifest + Mock -ModuleName $moduleForMock -CommandName Test-AlkamiServiceManifest10 -MockWith { return @{ success = $true } } + It "Calls Test-AlkamiServiceManifest10" { + Test-AlkamiManifest -Source "TestDrive:\NewServiceAlkamiManifest.xml" + + Assert-MockCalled -CommandName Test-AlkamiServiceManifest10 -Scope It + } + } + + Context "When Testing a Service Manifest with Release Management" { + New-FakeServiceManifestWithReleaseManagement + It "Returns true" { + Test-AlkamiManifest -Source "TestDrive:\NewServiceAlkamiManifest.xml" | Should -BeTrue + } + } + + Context "When Testing a FluentMigration manifest" { + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiManifestFilename -MockWith { return "NewFluentMigrationAlkamiManifest.xml" } + New-ValidFluentMigrationManifest + It "Returns true" { + Test-AlkamiManifest -Source "TestDrive:\NewFluentMigrationAlkamiManifest.xml" | Should -BeTrue + } + } + + Context "When Testing an invalid Service Manifest with Release Management" { + New-InvalidServiceManifestWithReleaseManagement + It "Returns false" { + Test-AlkamiManifest -Source "TestDrive:\NewServiceAlkamiManifest.xml" | Should -BeFalse + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.ps1 new file mode 100644 index 0000000..b30fb93 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.ps1 @@ -0,0 +1,63 @@ +function Test-AlkamiManifestGeneral10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the General portion of an AlkamiManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER General + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$General + ) + + $success = $true + $resultMessages = @() + + if ([string]::IsNullOrWhiteSpace($General.creatorCode)) { + $resultMessages += 'The packageManifest/general/creatorCode tag in this file is missing or empty. This should be present.' + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($General.element)) { + $resultMessages += 'The packageManifest/general/element tag in this file is missing or empty. This should be present.' + $success = $false + } + + if (-not [string]::IsNullOrWhiteSpace($General.releaseManagement.alwaysDeploy) -and -not (@('true','false') -contains $General.releaseManagement.alwaysDeploy)) { + $resultMessages += "packageManifest/general/releaseManagement/alwaysDeploy must be true or false, if a releaseManagement node is provided" + $success = $false + } + + if (-not (Test-IsCollectionNullOrEmpty $General.bankIdentifiers)) { + foreach($bankIdentifier in $General.bankIdentifiers.bankIdentifier) { + $guid = $bankIdentifier.'#text' + $name = $bankIdentifier.name + if (-not $name) { + $resultMessages += 'Bank identifier should have a name so that it can be resolved to the expected FI during investigation of errors.' + $success = $false + } + if (-not $guid) { + $resultMessages += 'The bankIdentifier value must be specified' + $success = $false + } + try { + ## Force the guid value to a guid type. This will error if it's invalid + ([Guid]$guid) | Out-Null + } + catch { + $resultMessages += 'The bankIdentifier guid must be parseable as a valid guid' + $success = $false + } + } + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.tests.ps1 new file mode 100644 index 0000000..64bfd98 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiManifestGeneral10.tests.ps1 @@ -0,0 +1,55 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiManifestGeneral10" { + Context "Test valid manifest" { + $result = Test-AlkamiManifestGeneral10 @{ + ComponentType = "Widget" + CreatorCode = "SRE" + Element = "Test" + ReleaseManagement = @{ + alwaysDeploy = $false + } + BankIdentifiers = @( + @{ + BankIdentifier = @{ + # This is how XML files get read in as a tag value + '#text' = ([Guid]::NewGuid()).Guid + Name = "Testing Randomness" + } + } + ) + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiManifestGeneral10 @{ + ComponentType = "invalid" + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least two warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 2 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.ps1 new file mode 100644 index 0000000..059a688 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.ps1 @@ -0,0 +1,64 @@ +function Test-AlkamiProviderManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the ProviderManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER ProviderManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$ProviderManifest + ) + + $success = $true + $resultMessages = @() + $providerAppInstall = @('BankService', 'Bank', 'CoreService', 'Core', 'Notify', 'Notification', 'NotificationService', 'SecurityManagementService', 'SecurityManagement', 'Security', 'Radium', 'Nag','NagConfigurationService','NagConfig','NagConfiguration','SchedulerService','All') + + $manifestAppInstalls = @(($ProviderManifest.appInstall -split ',') -split '\|') + + if ([string]::IsNullOrWhiteSpace($ProviderManifest.appInstall) -or (Test-IsCollectionNullOrEmpty -Collection $manifestAppInstalls)) { + $resultMessages += "packageManifest/providerManifest/appInstall must be non-empty and one of these values: $($providerAppInstall -join ',')" + $success = $false + } else { + ## The submitted manifestAppInstalls is an array because of the split options above. + ## Iterate over all items and ensure they exist in the ProviderAppInstall array. + ## Only select entries that don't exist in ProviderAppInstall + $allFound = @($manifestAppInstalls | Where-Object { $providerAppInstall -contains $_ } | Where-Object { $_ -eq $false }).Count -eq 0 + if (-not $allFound) { + $resultMessages += "packageManifest/providerManifest/appInstall must be one of these values: $($providerAppInstall -join ',')" + $success = $false + } + } + + if ([string]::IsNullOrWhiteSpace($ProviderManifest.providerType)) { + $resultMessages += "packageManifest/providerManifest/providerType must be non-empty" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($ProviderManifest.providerName)) { + $resultMessages += "packageManifest/providerManifest/providerName must be non-empty" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($ProviderManifest.pluginType)) { + $resultMessages += "packageManifest/providerManifest/pluginType must be non-empty" + $success = $false + } + + $validPluginType = @('Connector', 'Processor') + + if (-not ($validPluginType -contains $ProviderManifest.pluginType)) { + $resultMessages += "packageManifest/providerManifest/pluginType must be one of these values: $($validPluginType -join ',')" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.tests.ps1 new file mode 100644 index 0000000..b323981 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiProviderManifest10.tests.ps1 @@ -0,0 +1,44 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiProviderManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiProviderManifest10 @{ + appInstall = "Bank" + providerType = "test" + providerName = "test" + pluginType = "Processor" + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiProviderManifest10 @{ + appInstall = "" + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least two warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 2 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.ps1 new file mode 100644 index 0000000..b106d5d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.ps1 @@ -0,0 +1,25 @@ +function Test-AlkamiReportManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the ReportManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER ReportManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + # [ValidateNotNullOrEmpty()] + [object]$ReportManifest + ) + + $success = $true + $resultMessages = @() + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.tests.ps1 new file mode 100644 index 0000000..d012e4e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiReportManifest10.tests.ps1 @@ -0,0 +1,27 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiReportManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiReportManifest10 @{ + status = '' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + # Honestly, there are no viable properties on this manifest type yet. + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.ps1 new file mode 100644 index 0000000..22d9ed2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.ps1 @@ -0,0 +1,31 @@ +function Test-AlkamiRepositoryManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the RepositoryManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER RepositoryManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$RepositoryManifest + ) + + $success = $true + $resultMessages = @() + $validWebTierInstall = Get-ValidWebTierInstallLocations + + if ([string]::IsNullOrWhiteSpace($RepositoryManifest.appInstall) -or -not ($validWebTierInstall -contains $RepositoryManifest.appInstall)) { + $resultMessages += "packageManifest/repositoryManifest/appInstall contains an invalid value. Valid values are $($validWebTierInstall -join ',')" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.tests.ps1 new file mode 100644 index 0000000..c9a50ea --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiRepositoryManifest10.tests.ps1 @@ -0,0 +1,41 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiRepositoryManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiRepositoryManifest10 @{ + appInstall = 'client' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiRepositoryManifest10 @{ + appInstall = 'garbage' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.ps1 new file mode 100644 index 0000000..1d9af7a --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.ps1 @@ -0,0 +1,28 @@ +function Test-AlkamiSREModuleManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the SREModule Manifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER ModuleManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$ModuleManifest + ) + + $success = $true + $resultMessages = @() + + # SREModule manifests don't have testable structure that we care about at this time. + # This is ok. + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.tests.ps1 new file mode 100644 index 0000000..50fe581 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiSREModuleManifest10.tests.ps1 @@ -0,0 +1,23 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiSREModuleManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiSREModuleManifest10 @{ + anything = "is unused here" + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.ps1 new file mode 100644 index 0000000..f86efbe --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.ps1 @@ -0,0 +1,104 @@ +function Test-AlkamiServiceManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the ServiceManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER ServiceManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$ServiceManifest + ) + + $success = $true + $resultMessages = @() + $validServiceRuntimes = @('core', 'dotnetcore', 'legacy', 'framework') + + if ([string]::IsNullOrWhiteSpace($ServiceManifest.runtime)) { + $resultMessages += "packageManifest/serviceManifest/runtime must be non-empty" + $success = $false + } + + if (-not ($validServiceRuntimes -contains $ServiceManifest.runtime)) { + $resultMessages += "packageManifest/serviceManifest/runtime must be one of these values: $($validServiceRuntimes -join ',')" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($ServiceManifest.entryPoint)) { + $resultMessages += "packageManifest/serviceManifest/entryPoint must be non-empty" + $success = $false + } + + $migrations = $ServiceManifest.migrations + + # Assembly checks + if ($migrations -and $migrations.assembly) { + # We should have EITHER assemblies OR packages + if ($migrations.package) { + $resultMessages += "packageManifest/serviceManifest/migrations can only have an assembly, or a package list. Not both." + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($migrations.assembly.target)) { + $resultMessages += "packageManifest/serviceManifest/migrations/assembly/target must be non-empty, if a migration and assembly are provided." + $success = $false + } + } + + if ($migrations -and -not $migrations.assembly) { + # If we have no packages, we expect assemblies. + if (-not $migrations.package) { + $resultMessages += "packageManifest/serviceManifest/migrations must have one or more assembly, or package specified" + $success = $false + } + } + + $provider = $ServiceManifest.provider + if ($provider) { + if ([string]::IsNullOrWhiteSpace($provider.providerName)) { + $resultMessages += "packageManifest/serviceManifest/provider/providerName must be non-empty, if a provider node is provided." + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($provider.providerType)) { + $resultMessages += "packageManifest/serviceManifest/provider/providerType must be non-empty, if a provider node is provided." + $success = $false + } + } + + $foundSomePackages = $false + $packageErrors = @() + foreach ($package in $migrations.package) { + $foundSomePackages = $true + if ([string]::IsNullOrWhiteSpace($package.id)) { + $packageErrors += "Found an empty packageid value in the manifest for migrations" + } + if ([string]::IsNullOrWhiteSpace($package.version)) { + if (-not [string]::IsNullOrWhiteSpace($package.id)) { + $packageErrors += "Found an empty/missing package version value in the manifest for package id $($package.id)" + } else { + $packageErrors += "Found an empty/missing packageid value in the manifest" + } + } + } + + if ($foundSomePackages -and $packageErrors) { + $resultMessages += ($packageErrors -join '`n') + $success = $false + } + + if (($null -ne $ServiceManifest.db_role) -and [string]::IsNullOrWhiteSpace($ServiceManifest.db_role)) { + $resultMessages += "packageManifest/serviceManifest/db_role must be populated when provided" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.tests.ps1 new file mode 100644 index 0000000..dd18dc6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiServiceManifest10.tests.ps1 @@ -0,0 +1,263 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiServiceManifest10" { + function New-FakeServiceManifest { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-InvalidServiceManifest { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-ValidServiceManifestWithProvider { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + + my_provider_type + my_provider_name + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-InvalidServiceManifestWithProvider { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.Testing.FakeService + Service + + DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF + + + + framework + Alkami.MicroServices.Testing.FakeService + + + + + + + my_service_role + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + function New-InvalidServiceManifestWithAssemblyAndPackage { + $sampleManifest = @" + + + 1.0 + + Alkami + Alkami.MicroServices.FluxManagement.Service.Host + Service + + + framework + Alkami.MicroServices.FluxManagement.Service.Host + + + Alkami.MicroServices.FluxManagement.Migrations.dll + + + FluxManagement_Service + + +"@ + $testFile = "TestDrive:\NewServiceAlkamiManifest.xml" + + Set-Content -Path $testFile -value $sampleManifest + + } + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "SUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiManifestFilename -MockWith { return "NewServiceAlkamiManifest.xml" } + + # The expectation is that we will have multiple versions here. We might need more contexts per version at that point. + Context "When Testing a valid Service Manifest" { + New-FakeServiceManifest + It "Returns success == true" { + + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.success | Should -BeTrue + } + It "Does not return an array of errors." { + + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.results | Should -BeNullOrEmpty + } + } + + Context "When Testing an invalid Service Manifest" { + New-InvalidServiceManifest + + It "Returns success == false" { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + $results.success | Should -BeFalse + } + + It "Returns a non-empty array of errors." { + + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.results | Should -not -BeNullOrEmpty + } + } + + Context "When Testing a valid Service Manifest with provider" { + New-ValidServiceManifestWithProvider + It "Returns success == true" { + + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.success | Should -BeTrue + } + + It "Does not return an array of errors." { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.results | Should -BeNullOrEmpty + } + } + + Context "When Testing an invalid Service Manifest with provider" { + New-InvalidServiceManifestWithProvider + + It "Returns success == false" { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + $results.success | Should -BeFalse + } + + It "Returns a non-empty array of errors." { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.results | Should -not -BeNullOrEmpty + } + } + + Context "When Testing a Service Manifest with assembly and package" { + New-InvalidServiceManifestWithAssemblyAndPackage + + It "Returns success == false" { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + $results.success | Should -BeFalse + } + + It "Returns a non-empty array of errors." { + $file = Get-Content "TestDrive:\NewServiceAlkamiManifest.xml" + $xml = [xml]$file + $results = Test-AlkamiServiceManifest10 $xml.packageManifest.serviceManifest + + $results.results | Should -not -BeNullOrEmpty + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.ps1 new file mode 100644 index 0000000..ed6997e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.ps1 @@ -0,0 +1,54 @@ +function Test-AlkamiWebApplicationManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the WebApplicationManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER WebApplicationManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$WebApplicationManifest + ) + + $success = $true + $resultMessages = @() + # SRE-17748 failed on the below, leaving as breadcrumbs so we don't do it again + # Do not do the below line, as WebApps are _also_ valid on Legacy = App tier + # $validWebApplicationInstall = Get-ValidWebTierInstallLocations + $validWebApplicationInstall = 'Client', 'Admin', 'Generic', 'Legacy' + + if ([string]::IsNullOrWhiteSpace($WebApplicationManifest.appInstall)) { + $resultMessages += "packageManifest/webApplicationManifest/appInstall must be non-empty" + $success = $false + } + + if (-not ($validWebApplicationInstall -contains $WebApplicationManifest.appInstall)) { + $resultMessages += "packageManifest/webApplicationManifest/appInstall contains an invalid value. Valid values are $($validWebApplicationInstall -join ',')" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($WebApplicationManifest.appName)) { + $resultMessages += "packageManifest/webApplicationManifest/appName must be non-empty" + $success = $false + } + + if (-not [string]::IsNullOrWhiteSpace($WebApplicationManifest.needsShared) -and -not (@('true','false') -contains $WebApplicationManifest.needsShared)) { + $resultMessages += "packageManifest/webApplicationManifest/needsShared must be true or false" + $success = $false + } + + if (-not [string]::IsNullOrWhiteSpace($WebApplicationManifest.noManagedCode) -and -not (@('true','false') -contains $WebApplicationManifest.noManagedCode)) { + $resultMessages += "packageManifest/noManagedCode/noManagedCode must be true or false" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.tests.ps1 new file mode 100644 index 0000000..f6eae3d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebApplicationManifest10.tests.ps1 @@ -0,0 +1,44 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiWebApplicationManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiWebApplicationManifest10 @{ + appInstall = 'client' + appName = "test" + noManagedCode = $true + needsShared = $true + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiWebApplicationManifest10 @{ + appInstall = 'garbage' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least two warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 2 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.ps1 new file mode 100644 index 0000000..68c48e7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.ps1 @@ -0,0 +1,36 @@ +function Test-AlkamiWebExtensionManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the WebExtensionManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER WebExtensionManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$WebExtensionManifest + ) + + $success = $true + $resultMessages = @() + $validWebTierInstall = Get-ValidWebTierInstallLocations + + if ([string]::IsNullOrWhiteSpace($WebExtensionManifest.appInstall)) { + $resultMessages += "packageManifest/webExtensionManifest/appInstall must be present and contain a valid value." + $success = $false + } + + if ($validWebTierInstall -notcontains $WebExtensionManifest.appInstall) { + $resultMessages += "packageManifest/webExtensionManifest/appInstall contains an invalid value. Valid values are $($validWebTierInstall -join ',')" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.tests.ps1 new file mode 100644 index 0000000..e10de62 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebExtensionManifest10.tests.ps1 @@ -0,0 +1,41 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiWebExtensionManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiWebExtensionManifest10 @{ + appInstall = 'client' + } + + It "Tested successfully" { + $result.success | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiWebExtensionManifest10 @{ + appInstall = 'garbage' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.ps1 new file mode 100644 index 0000000..dec4d2c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.ps1 @@ -0,0 +1,35 @@ +function Test-AlkamiWebsiteManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the WebsiteManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER WebsiteManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$WebsiteManifest + ) + + $success = $true + $resultMessages = @() + + if ([string]::IsNullOrWhiteSpace($WebsiteManifest.websiteName)) { + $resultMessages += "packageManifest/websiteManifest/websiteName can not be empty" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($WebsiteManifest.url)) { + $resultMessages += "packageManifest/websiteManifest/url can not be empty" + $success = $false + } + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.tests.ps1 new file mode 100644 index 0000000..111b00d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWebsiteManifest10.tests.ps1 @@ -0,0 +1,43 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiWebsiteManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiWebsiteManifest10 @{ + websiteName = "test" + url = "http://example.com" + } + + It "Tested successfully" { + $result | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiWebsiteManifest10 @{ + websiteName = "garbage" + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least one warning statements" { + # Website name is technically a valid name, so just one error here + $result.results.Count | Should -BeGreaterOrEqual 1 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.ps1 new file mode 100644 index 0000000..ff5a522 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.ps1 @@ -0,0 +1,95 @@ +function Test-AlkamiWidgetManifest10 { +<# +.SYNOPSIS + Please don't use this file by hand, please use Test-AlkamiManifest + This function is intended to validate the WidgetManifest dotted object/hashtable so we can ensure that the values provided meet a minimum standard of valid + +.PARAMETER WidgetManifest + A dotted object ([xml](Get-Content -Path $somePath)) or hashtable of values +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$WidgetManifest + ) + + $success = $true + $resultMessages = @() + $validWebTierInstall = Get-ValidWebTierInstallLocations + + if ([string]::IsNullOrWhiteSpace($widgetManifest.widgetInstall)) { + $resultMessages += 'PackageManifest/widgetManifest/widgetInstall not found. This element is required for widgets.' + $success = $false + } + + if ($validWebTierInstall -notcontains $widgetManifest.widgetInstall) { + $resultMessages += "packageManifest/widgetManifest/widgetInstall contains an invalid value. Valid values are $($validWebTierInstall -join ',')" + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($widgetManifest.areaName)) { + $resultMessages += 'PackageManifest/widgetManifest/areaName not found. This element is required for widgets.' + $success = $false + } + + if ([string]::IsNullOrWhiteSpace($widgetManifest.assemblyInfo)) { + $resultMessages += 'PackageManifest/widgetManifest/assemblyInfo not found. This element is required for widgets.' + $success = $false + } + + <# Per SRE-13338 ~ Comment out the widget unsupported functionality until we can re-visit with an updated set of usage requirements. + if (-not $widgetManifest.displaySettings) { + $resultMessages += 'PackageManifest/widgetManifest/displaySettings not found. This element is required for widgets.' + } + + ## Reference: https://bitbucket.corp.alkamitech.com/projects/APPDEV/repos/orb/browse/Common/Alkami.App.Core.Contracts/DataContracts/WidgetDisplaySettings.cs + $validWidgetDisplaySettings = @('Desktop', 'Mobile', 'Tablet', 'DesktopMobile', 'DesktopTablet', 'All', 'Hidden', 'None') + + if (-not ($validWidgetDisplaySettings -contains $widgetManifest.displaySettings)) { + $resultMessages += "packageManifest/widgetManifest/displaySettings contains an invalid value. Valid values are $($validWidgetDisplaySettings -join ',')" + } + + if ($null -ne $widgetManifest.menuItems) { + $hasMenuItems = $false + foreach($menuItem in $widgetManifest.menuItems.menuItem) { + $hasMenuItems = $true + + if (-not $menuItem.menuItemType) { + $resultMessages += 'PackageManifest/widgetManifest/menuItems/menuItem/menuItemType must be specified.' + } + + if ($menuItem.menuItemType -ne 'Admin') { + $resultMessages += 'PackageManifest/widgetManifest/menuItems/menuItem/menuItemType is only supported as Admin at this time.' + } + + if (-not $menuItem.key) { + $resultMessages += 'PackageManifest/widgetManifest/menuItems/menuItem/key must be specified.' + } + + if (-not $menuItem.link) { + $resultMessages += 'PackageManifest/widgetManifest/menuItems/menuItem/link must be specified.' + } + + $mustMatchPermissions = $menuItem.mustMatchPermissions -eq 'True' + $hasPermissions = $false + if ($mustMatchPermissions -and $null -ne $menuItem.permissions) { + ## We should probably figure out a way to validate the permissions provided in this list, but that may be impossible. + $hasPermissions =-not (Test-IsCollectionNullOrEmpty $menuItem.permissions.permission) + } + if ($mustMatchPermissions -and -not $hasPermissions) { + $resultMessages += 'PackageManifest/widgetManifest/menuItems/menuItem/permissions/permission must be specified when packageManifest/widgetManifest/menuItems/menuItem/mustMatchPermissions is set to true.' + } + } + if (-not $hasMenuItems) { + $resultMessages += 'Specifying the packageManifest/widgetManifest/menuItems tag requires children menuItem records as well' + } + } + #> + + return @{ + success = $success + results = $resultMessages + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.tests.ps1 new file mode 100644 index 0000000..6de95fd --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-AlkamiWidgetManifest10.tests.ps1 @@ -0,0 +1,43 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-AlkamiWidgetManifest10" { + Context "Test valid manifest" { + $result = Test-AlkamiWidgetManifest10 @{ + widgetInstall = 'client' + areaName = 'test' + assemblyInfo = 'test' + } + + It "Tested successfully" { + $result | Should -BeTrue + } + + It "Produced no error messages" { + $result.results.Count | Should -Be 0 + } + } + + Context "Test invalid manifest" { + $result = Test-AlkamiWidgetManifest10 @{ + widgetInstall = 'garbage' + } + + It "Tested successfully" { + $result.success | Should -BeFalse + } + + It "Actually produced warnings" { + $result.results | Should -Not -BeNullOrEmpty + } + + It "Produced at least two warning statements" { + $result.results.Count | Should -BeGreaterOrEqual 2 + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.ps1 new file mode 100644 index 0000000..1f31365 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.ps1 @@ -0,0 +1,30 @@ +Function Test-IsAppServer { +<# +.SYNOPSIS + Used to determine if the current server is an App tier server + +.PARAMETER ComputerName + [string] Optional. Specify a computer name instead of inspecting the local computer name. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = $null + ) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + $ComputerName = (Get-FullyQualifiedServerName) + } + + # Then let's check the server name + if ($ComputerName.ToUpper().StartsWith("APP")) { + return $true + } + + # Put any future checks here + + return $false +} + +Set-Alias IsAppServer Test-IsAppServer -Force -Scope:Global diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.tests.ps1 new file mode 100644 index 0000000..96e0e3e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAppServer.tests.ps1 @@ -0,0 +1,95 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsAppServer" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + + Context "Parameter is passed" { + It "Returns true if passed an app server name" { + Test-IsAppServer -ComputerName "APP123456" | Should -BeTrue + } + + It "Returns false if passed a web server name" { + Test-IsAppServer -ComputerName "WEB123456" | Should -BeFalse + } + + It "Returns false if passed a mic server name" { + Test-IsAppServer -ComputerName "MIC123456" | Should -BeFalse + } + + It "Returns false if passed a dell laptop name" { + Test-IsAppServer -ComputerName "ALK-DELL1234" | Should -BeFalse + } + } + + Context "Uses empty ComputerName parameter, delivers right result" { + It "Returns true if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsAppServer -ComputerName "" | Should -BeTrue + } + + It "Returns false if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsAppServer -ComputerName "" | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsAppServer -ComputerName "" | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsAppServer -ComputerName "" | Should -BeFalse + } + } + + Context "Uses null ComputerName parameter, delivers right result" { + It "Returns true if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsAppServer -ComputerName $null | Should -BeTrue + } + + It "Returns false if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsAppServer -ComputerName $null | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsAppServer -ComputerName $null | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsAppServer -ComputerName $null | Should -BeFalse + } + } + + Context "Uses no ComputerName parameter, delivers right result" { + It "Returns true if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsAppServer | Should -BeTrue + } + + It "Returns false if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsAppServer | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsAppServer | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsAppServer | Should -BeFalse + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.ps1 new file mode 100644 index 0000000..111180f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.ps1 @@ -0,0 +1,29 @@ +Function Test-IsAws { + <# + .SYNOPSIS + Returns $true if in AWS (based on meta-data endpoint check) and $false if not + #> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + $endpoint = "/meta-data/hostname" + + # Try reading the metadata endpoint + try { + $awsCheck = (Get-InstanceMetadata -Endpoint $endpoint) + if ($null -ne $awsCheck.Content) { + return $true + } + } catch { + Write-Verbose ("$logLead : AWS Endpoint Check Returned Error: {0}" -f $_.Exception.Message) + } + + # Query OS WMI Information + $organizationResult = (Get-CIMInstance -ClassName Win32_OperatingSystem -Namespace "root\CIMV2" ` + -Property Organization -ErrorAction SilentlyContinue).Organization + + return ($organizationResult -match "amazon") +} + +Set-Alias IsAws Test-IsAws -Force -Scope:Global \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.tests.ps1 new file mode 100644 index 0000000..a1f27f9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsAws.tests.ps1 @@ -0,0 +1,131 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsAws" { + + Context "Error Handling" { + + It "Does Not Throw If the AWS Metadata Endpoint Errors" { + + Mock -CommandName Get-InstanceMetadata -MockWith { + + throw "This is a Throw In the Mock" + + } -ModuleName $moduleForMock + + { Test-IsAws } | Should -Not -Throw + Assert-MockCalled -CommandName Get-InstanceMetadata -ModuleName $moduleForMock ` + -Scope It -Times 1 -Exactly + } +<# + It "Contains a Short Timeout for Web Requests in case the Endpoint Doesn't Respond" { + + Mock -CommandName Invoke-WebRequest -MockWith { + + # Sleep for the number of seconds that Test-IsAws Sets the Timeout To + Start-Sleep -Seconds $TimeoutSec + throw "This is a Throw In the Mock Again" + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -MockWith {} -ModuleName $moduleForMock + + Measure-Command { Test-IsAws } | Select-Object -ExpandProperty TotalMilliseconds | Should -Not -BeGreaterThan 6000 + Assert-MockCalled -CommandName Invoke-WebRequest -ModuleName $moduleForMock ` + -Scope It -Times 1 -Exactly + } +#> + } + + Context "Data Evaluation" { + + It "Returns False if the AWS Endpoint Check Throws" { + + Mock -CommandName Get-InstanceMetadata -MockWith { + + throw "This is a Throw In the Mock" + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -MockWith {} -ModuleName $moduleForMock + + Test-IsAws | Should -BeFalse + Assert-MockCalled -CommandName Get-InstanceMetadata -ModuleName $moduleForMock ` + -Scope It -Times 1 -Exactly + } + + It "Returns False if the AWS Endpoint Check Contains No Content in the Response" { + + Mock -CommandName Get-InstanceMetadata -MockWith { + + [PSCustomObject]@{ + + Content = $null + } + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -MockWith {} -ModuleName $moduleForMock + + Test-IsAws | Should -BeFalse + Assert-MockCalled -CommandName Get-InstanceMetadata -ModuleName $moduleForMock ` + -Scope It -Times 1 -Exactly + } + + It "Returns True if the AWS Endpoint Check Contains Content in the Response" { + + Mock -CommandName Get-InstanceMetadata -MockWith { + + [PSCustomObject]@{ + + Content = "Hi There I'm Amazon" + } + + } -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -MockWith {} -ModuleName $moduleForMock + + Test-IsAws -Verbose | Should -BeTrue + Assert-MockCalled -CommandName Get-InstanceMetadata -ModuleName $moduleForMock ` + -Scope It -Times 1 -Exactly + } + + It "Returns False if the OS Organization Does Not Match AWS" { + + Mock -CommandName Get-InstanceMetadata -MockWith { return $null} -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -ModuleName $moduleForMock -MockWith { + + return [PSCustomObject]@{ + Organization = "Pester WorldWide" + } + + } + Test-IsAws | Should -BeFalse + + Assert-MockCalled -CommandName Get-InstanceMetadata -Times 1 -ModuleName $moduleForMock -Scope It + Assert-MockCalled -CommandName Get-CIMInstance -ModuleName $moduleForMock -Scope It -Times 1 + } + + It "Returns True if the OS Organization Matches AWS" { + + Mock -CommandName Get-InstanceMetadata -MockWith { return $null} -ModuleName $moduleForMock + + Mock -CommandName Get-CIMInstance -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Organization = "Amazon.com" + } + + } + + Test-IsAws | Should -BeTrue + Assert-MockCalled -CommandName Get-CIMInstance -ModuleName $moduleForMock -Scope It -Times 1 -Exactly + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsBuildEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsBuildEnvironment.ps1 new file mode 100644 index 0000000..a6e1de7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsBuildEnvironment.ps1 @@ -0,0 +1,20 @@ +function Test-IsBuildEnvironment { +<# +.SYNOPSIS + Determine if this environment is configured as a build environment + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('Build') + + return ($environmentTypeNames -contains $environmentType) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.Tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.Tests.ps1 new file mode 100644 index 0000000..a1418b7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.Tests.ps1 @@ -0,0 +1,82 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsDeveloperMachine" { + + Mock Select-AlkamiTeaServers -ModuleName $moduleForMock -MockWith {} + Mock Select-AlkamiAppServers -ModuleName $moduleForMock -MockWith {} + Mock Select-AlkamiMicServers -ModuleName $moduleForMock -MockWith {} + Mock Select-AlkamiWebServers -ModuleName $moduleForMock -MockWith {} + + Context "testing production configuration with global constant" { + It "Returns false for Environment.Type QA" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return "QA" } + + Test-IsDeveloperMachine | Should -Be $false + } + } + + Context "testing production configuration with random string" { + It "Returns false for Environment.Type production" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return "Production" } + + Test-IsDeveloperMachine | Should -Be $false + } + } + + <# ********************* #> + + Context "testing empty or developer configurations" { + + It "Returns true for Environment.Type Developer" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return 'Development' } + + Test-IsDeveloperMachine | Should -Be $true + } + + It "Returns true for Environment.Type empty string" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return "" } + + Test-IsDeveloperMachine | Should -Be $true + } + + It "Returns true for Environment.Type null" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return $null } + + Test-IsDeveloperMachine | Should -Be $true + } + + It "Returns false for APP tier server in AWS Dev account" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Select-AlkamiAppServers -ModuleName $moduleForMock -MockWith { $true } + + Test-IsDeveloperMachine | Should -Be $false + } + + It "Returns false for MIC tier server in AWS Dev account" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Select-AlkamiMicServers -ModuleName $moduleForMock -MockWith { $true } + + Test-IsDeveloperMachine | Should -Be $false + } + + It "Returns false for WEB tier server in AWS Dev account" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Select-AlkamiWebServers -ModuleName $moduleForMock -MockWith { $true } + + Test-IsDeveloperMachine | Should -Be $false + } + + It "Returns false for TEA tier server in AWS Dev account" { + Mock -CommandName Get-EnvironmentType -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Select-AlkamiTeaServers -ModuleName $moduleForMock -MockWith { $true } + + Test-IsDeveloperMachine | Should -Be $false + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.ps1 new file mode 100644 index 0000000..ce0bb99 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsDeveloperMachine.ps1 @@ -0,0 +1,33 @@ +function Test-IsDeveloperMachine { +<# +.SYNOPSIS + Is the current machine a developer's environment or a QA/Staging/Production environment? + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('Development', 'Unknown', 'SDK', 'Sandbox', '', $null) + + if ((Select-AlkamiTeaServers -Servers $ComputerName) -or + (Select-AlkamiAppServers -Servers $ComputerName) -or + (Select-AlkamiMicServers -Servers $ComputerName) -or + (Select-AlkamiWebServers -Servers $ComputerName)) { + return $false + } else { + return ($environmentTypeNames -contains $environmentType) + } +} + +Set-Alias -Name Test-IsDevelopmentEnvironment -Value Test-IsDeveloperMachine + +# If the tag doesn't exist this is a development machine or should be treated as such. +# This is the only place that we can accept a missing env-type var +# return ([string]::IsNullOrWhiteSpace($environmentType)) -or ($environmentTypeNames -contains $environmentType) \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.ps1 new file mode 100644 index 0000000..d444e63 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.ps1 @@ -0,0 +1,37 @@ +Function Test-IsEntrustServer { +<# +.SYNOPSIS + Used to determine if the current server is an Entrust server + +.DESCRIPTION + Checks for the existence of the Entrust service and folders +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param() + + $logLead = Get-LogLeadName + + # Lookup Entrust by its default Name + Write-Verbose "$logLead : Looking for Entrust Service" + $entrustService = Get-Service -Name IdentityGuard -ErrorAction SilentlyContinue + Write-Verbose ("$logLead : Found {0} Matching Services" -f $entrustService.Count) + + if ($null -ne $entrustService) { + Write-Verbose "$logLead : Returning true because Entrust IdentityGuard service located" + return $true + } + + # Lookup Entrust Directory + Write-Verbose "$logLead : Looking for Entrust Installation Folder" + $entrustFolder = Get-ChildItem -Directory -Path @("$ENV:ProgramFiles", "${ENV:ProgramFiles(x86)}") -Filter "Entrust" + Write-Verbose ("$logLead : Found {0} Matching Folders" -f $entrustFolder.Count) + + if ($null -ne $entrustFolder) { + Write-Verbose "$logLead : Returning true because Entrust folder located" + return $true + } + + Write-Verbose "$logLead : All checks failed, returning false" + return $false +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.tests.ps1 new file mode 100644 index 0000000..793941c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEntrustServer.tests.ps1 @@ -0,0 +1,59 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsEntrustServer" { + + Context "Entrust Checks" { + + It "Returns true if the IdentityGuard Service is Located" { + + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return "FakeService" } + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + + Test-IsEntrustServer -Verbose | Should -BeTrue + # We called Get-Service + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Service -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Name -contains "IdentityGuard" } + # ... but not Get-ChildItem + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 0 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Verbose -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Returning true because Entrust IdentityGuard service located" } + } + + It "Returns true if the Entrust folder is located on disk" { + + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem -MockWith { return "ImAnEntrustFolder" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + + Test-IsEntrustServer -Verbose | Should -BeTrue + # We called Get-Service + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Service -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Name -contains "IdentityGuard" } + # ... and Get-ChildItem + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Verbose -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Returning true because Entrust folder located" } + } + + It "Returns false if neither the Entrust Service or Folder is Found" { + + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + + Test-IsEntrustServer -Verbose | Should -BeFalse + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Service -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Name -contains "IdentityGuard" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ChildItem -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Verbose -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "All checks failed" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.ps1 new file mode 100644 index 0000000..ae771da --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.ps1 @@ -0,0 +1,29 @@ +function Test-IsEnvironment { +<# +.SYNOPSIS + Check if a computer is a certain environment type + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet('Production', 'QA', 'Development', 'LoadTest', 'Build', 'Unconfigured')] + [string]$EnvironmentType, + [string]$ComputerName = 'localhost' + ) + + $typeMatches = switch ($EnvironmentType) { + 'Production' { return Test-IsProductionEnvironment -ComputerName $ComputerName} + 'QA' { return Test-IsQAEnvironment -ComputerName $ComputerName} + 'Development' { return Test-IsDeveloperMachine -ComputerName $ComputerName} + 'LoadTest' { return Test-IsLoadTestEnvironment -ComputerName $ComputerName} + 'Build' { return Test-IsBuildEnvironment -ComputerName $ComputerName} + 'Unconfigured' { return Test-IsUnconfiguredEnvironment -ComputerName $ComputerName} + default { throw "what is $EnvironmentType" } + } + + return $typeMatches +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.tests.ps1 new file mode 100644 index 0000000..03591ed --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsEnvironment.tests.ps1 @@ -0,0 +1,98 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +#Import-Module $functionPath -Force +$moduleForMock = "Alkami.PowerShell.Configuration" # not working if I don't specify it for some reason? + +Describe "Test-IsEnvironment" { + Context "developer mocking tests (developer has excess odd cases)" { + It "is development" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "" } + Test-IsEnvironment -EnvironmentType Development | Should -Be $true + } + + It "is development" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return $null } + Test-IsEnvironment -EnvironmentType Development | Should -Be $true + } + + It "is development" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "development" } + Test-IsEnvironment -EnvironmentType Development | Should -Be $true + } + + It "is development" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "sdk" } + Test-IsEnvironment -EnvironmentType Development | Should -Be $true + } + + It "is development" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "this is not a real value" } + Test-IsEnvironment -EnvironmentType Development | Should -Be $false + } + } + + Context "production test" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "Production" } + + It "is production" { + Test-IsEnvironment -EnvironmentType Production | Should -Be $true + } + + It "is not qa when it is production" { + Test-IsEnvironment -EnvironmentType QA | Should -Be $false + } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-EnvironmentType -Times 2 + } + + Context "for empty" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "" } + + It "is not production" { + Test-IsEnvironment -EnvironmentType Production | Should -Be $false + } + + It "is not qa when it is production" { + Test-IsEnvironment -EnvironmentType QA | Should -Be $false + } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-EnvironmentType -Times 2 + } + + Context "Bad input value" { + Mock -ModuleName $moduleForMock -CommandName Get-EnvironmentType -MockWith { return "not a real environment" } + Mock -ModuleName Alkami.PowerShell.Configuration -CommandName Get-EnvironmentType -MockWith { return "not a real environment" } + Mock -ModuleName Alkami.PowerShell.Configuration -CommandName Get-AppSetting -MockWith { return "not a real value" } + + It "is not production" { + Test-IsEnvironment -EnvironmentType Production | Should -Be $false + } + + It "is not qa" { + Test-IsEnvironment -EnvironmentType QA | Should -Be $false + } + + It "is not Development" { + Test-IsEnvironment -EnvironmentType Development | Should -Be $false + } + + It "is not LoadTest" { + Test-IsEnvironment -EnvironmentType LoadTest | Should -Be $false + } + + It "is not build" { + Test-IsEnvironment -EnvironmentType Build | Should -Be $false + } + + It "is not Unconfigured" { + Test-IsEnvironment -EnvironmentType Unconfigured | Should -Be $false + } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-EnvironmentType -Times 5 + Assert-MockCalled -ModuleName Alkami.PowerShell.Configuration -CommandName Get-EnvironmentType -Times 1 + Assert-MockCalled -ModuleName Alkami.PowerShell.Configuration -CommandName Get-AppSetting -Times 1 + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.ps1 new file mode 100644 index 0000000..8e0e52d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.ps1 @@ -0,0 +1,31 @@ +Function Test-IsMicServer { +<# +.SYNOPSIS + Used to determine if the current server is a Microservice server. + +.PARAMETER ComputerName + [string] Optional. Specify a computer name instead of inspecting the local computer name. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = $null + ) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + $ComputerName = (Get-FullyQualifiedServerName) + } + + # Then let's check the server name + if ($ComputerName.ToUpper().StartsWith("MIC")) { + return $true + } + + # Put any future checks here + + return $false +} + +Set-Alias IsMicroServer Test-IsMicServer -Force -Scope:Global +Set-Alias Test-IsMicroServer Test-IsMicServer -Force -Scope:Global diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.tests.ps1 new file mode 100644 index 0000000..b612907 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsMicServer.tests.ps1 @@ -0,0 +1,95 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsMicServer" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + + Context "Parameter is passed" { + It "Returns false if passed an app server name" { + Test-IsMicServer -ComputerName "APP123456" | Should -BeFalse + } + + It "Returns true if passed a web server name" { + Test-IsMicServer -ComputerName "WEB123456" | Should -BeFalse + } + + It "Returns false if passed a mic server name" { + Test-IsMicServer -ComputerName "MIC123456" | Should -BeTrue + } + + It "Returns false if passed a dell laptop name" { + Test-IsMicServer -ComputerName "ALK-DELL1234" | Should -BeFalse + } + } + + Context "Uses empty ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsMicServer -ComputerName "" | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsMicServer -ComputerName "" | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsMicServer -ComputerName "" | Should -BeTrue + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsMicServer -ComputerName "" | Should -BeFalse + } + } + + Context "Uses null ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsMicServer -ComputerName $null | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsMicServer -ComputerName $null | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsMicServer -ComputerName $null | Should -BeTrue + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsMicServer -ComputerName $null | Should -BeFalse + } + } + + Context "Uses no ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsMicServer | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsMicServer | Should -BeFalse + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsMicServer | Should -BeTrue + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsMicServer | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.ps1 new file mode 100644 index 0000000..8d0cc5d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.ps1 @@ -0,0 +1,39 @@ +function Test-IsOverflowServer { + <# + .SYNOPSIS + Returns a boolean indicating if the current machine is an overflow or not. + + .DESCRIPTION + This will check the EC2 tags for a server and look for the key value of alk:overflow. + Will return true if alk:overflow:true is found and false in any other scenario. + + .EXAMPLE + # Testing on a standard server + Test-IsOverflowServer + False + + .EXAMPLE + # Testing on an overflow server + Test-IsOverflowServer + True + #> + + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + $logLead = (Get-LogLeadName) + + try { + $isOverflowServer = Get-CurrentInstanceTags -tagName "alk:overflow" -ValueOnly + + if ($isOverflowServer -eq 'true') { + return $true + } else { + return $false + } + } catch { + Write-Warning "$logLead`: Unable to query tags. This needs to be run on an AWS Server or the environment is set up incorrectly." + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.tests.ps1 new file mode 100644 index 0000000..f3daf6c --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsOverflowServer.tests.ps1 @@ -0,0 +1,39 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsOverflowServer" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + + Context "Successfull call of Get-CurrentInstanceTags" { + + It "Returns `$true for Overflow Tag on Overflow server" { + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags -MockWith { return "true" } + Test-IsOverflowServer | Should Be $true + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It + } + + It "Returns `$false for Overflow Tag on a non-Overflow server" { + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags -MockWith { return } + Test-IsOverflowServer | Should Be $false + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It + } + } + + Context "Failed call of Get-CurrentInstanceTags" { + + It "Calls Write-Warning when failling to retrieve tags" { + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags -MockWith { throw "Generic Error" } + Test-IsOverflowServer + Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Write-Warning -Times 1 -Exactly -Scope It + } + + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsProductionEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsProductionEnvironment.ps1 new file mode 100644 index 0000000..ac576fc --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsProductionEnvironment.ps1 @@ -0,0 +1,20 @@ +function Test-IsProductionEnvironment { +<# +.SYNOPSIS + Determine if this environment is configured as a production environment + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('Production', 'LoadTest', 'Staging') + + return ($environmentTypeNames -contains $environmentType) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsQaEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsQaEnvironment.ps1 new file mode 100644 index 0000000..746dc73 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsQaEnvironment.ps1 @@ -0,0 +1,20 @@ +function Test-IsQaEnvironment { +<# +.SYNOPSIS + Determine if this environment is configured as a QA environment + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('QA', 'TeamQA', 'Integration', 'Test', 'Regression') + + return ($environmentTypeNames -contains $environmentType) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsSecureEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsSecureEnvironment.ps1 new file mode 100644 index 0000000..17674d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsSecureEnvironment.ps1 @@ -0,0 +1,20 @@ +function Test-IsSecureEnvironment { +<# +.SYNOPSIS + Determine if this environment is configured as a secure environment + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('Secure') + + return ($environmentTypeNames -contains $environmentType) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.ps1 new file mode 100644 index 0000000..c3454fd --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.ps1 @@ -0,0 +1,35 @@ +Function Test-IsServiceFabricServer { + <# +.SYNOPSIS + Used to determine if the current server is a Service Fabric server. +#> + [CmdletBinding()] + [OutputType([bool])] + param( + ) + + $serverRole = Get-ServerRoleEnvironmentalVariable + + # Check Environmental Variables first + if ($serverRole -eq "fabric") { + + return $true + } elseif ($serverRole -match ("^(web|app|microservice)$")) { + + return $false + } + + # Then let's check the server name + if ($env:ComputerName.StartsWith("FAB")) { + + return $true; + } + + # If all else fails, check the alk:role tag if this is an AWS instance + if ((Test-IsAws) -and ((Get-CurrentInstanceTags "alk:role" -ValueOnly) -eq "app:fab")) { + + return $true + } + + return $false +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.tests.ps1 new file mode 100644 index 0000000..4d76f7f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsServiceFabricServer.tests.ps1 @@ -0,0 +1,110 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +$originalMachineName = $env:COMPUTERNAME + +Describe "Test-IsServiceFabricServer" { + + Context "Logic" { + + It "Returns True and Does Not Query AWS if the Server Role Environment Variable is fabric" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return "fabric" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "InvalidTagValue" } + + Test-IsServiceFabricServer | Should -BeTrue + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + + $nonMicEnvVars = @("Web", "App", "Microservice") + foreach ($Global:envVar in $nonMicEnvVars) { + + It "Returns False and Does Not Query AWS if the Server Role Environment Variable is $envVar" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return $Global:envVar } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "InvalidTagValue" } + + Test-IsServiceFabricServer | Should -BeFalse + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + } + } + + + It "Returns True and Does Not Query AWS if the Machine Name Starts with FAB" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return "InvalidRole" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "InvalidTagValue" } + + $env:COMPUTERNAME = "FAB123456" + Test-IsServiceFabricServer | Should -BeTrue + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + $env:COMPUTERNAME = $originalMachineName + } + + It "Returns False and Does Not Query AWS if the Machine Name Does Not Start with MIC and Test-IsAws Is False" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return "InvalidRole" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "app:fab" } + + $env:COMPUTERNAME = "WEB123456" + Test-IsServiceFabricServer | Should -BeFalse + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 0 -Exactly -Scope It + $env:COMPUTERNAME = $originalMachineName + } + + It "Returns True if the AWS Tag app:role has a Value of app:fab" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return "InvalidRole" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "app:fab" } + + $env:COMPUTERNAME = "WEB123456" + Test-IsServiceFabricServer | Should -BeTrue + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 1 -Exactly -Scope It ` + -ParameterFilter { $tagName -eq "alk:role" } + $env:COMPUTERNAME = $originalMachineName + } + + It "Returns False if the AWS Tag app:role has a Value Other Than app:fab" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServerRoleEnvironmentalVariable ` + -MockWith {return "InvalidRole" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws ` + -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Get-CurrentInstanceTags ` + -MockWith { return "InvalidTagValue" } + + $env:COMPUTERNAME = "WEB123456" + Test-IsServiceFabricServer | Should -BeFalse + Assert-MockCalled -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -Times 1 -Exactly -Scope It ` + -ParameterFilter { $tagName -eq "alk:role" } + $env:COMPUTERNAME = $originalMachineName + } + } +} + +$env:COMPUTERNAME = $originalMachineName \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsUnconfiguredEnvironment.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsUnconfiguredEnvironment.ps1 new file mode 100644 index 0000000..2b6e501 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsUnconfiguredEnvironment.ps1 @@ -0,0 +1,20 @@ +function Test-IsUnconfiguredEnvironment { +<# +.SYNOPSIS + Determine if this environment is configured as an unconfigured environment + +.PARAMETER ComputerName + What computer do we check, defaults to localhost +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [string]$ComputerName = 'localhost' + ) + + $environmentType = (Get-EnvironmentType -ComputerName $ComputerName) + + $environmentTypeNames = @('Unconfigured') + + return ($environmentTypeNames -contains $environmentType) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.ps1 new file mode 100644 index 0000000..6020e6f --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.ps1 @@ -0,0 +1,30 @@ +Function Test-IsWebServer { +<# +.SYNOPSIS + Used to determine if the current server is a Web tier server + +.PARAMETER ComputerName + [string] Optional. Specify a computer name instead of inspecting the local computer name. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $false)] + [string]$ComputerName = $null + ) + + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + $ComputerName = (Get-FullyQualifiedServerName) + } + + # Then let's check the server name + if ($ComputerName.ToUpper().StartsWith("WEB")) { + return $true + } + + # Put any future checks here + + return $false +} + +Set-Alias IsWebServer Test-IsWebServer -Force -Scope:Global diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.tests.ps1 new file mode 100644 index 0000000..bb1d6f5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWebServer.tests.ps1 @@ -0,0 +1,95 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsWebServer" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "[UUT]" } + + Context "Parameter is passed" { + It "Returns false if passed an app server name" { + Test-IsWebServer -ComputerName "APP123456" | Should -BeFalse + } + + It "Returns true if passed a web server name" { + Test-IsWebServer -ComputerName "WEB123456" | Should -BeTrue + } + + It "Returns false if passed a mic server name" { + Test-IsWebServer -ComputerName "MIC123456" | Should -BeFalse + } + + It "Returns false if passed a dell laptop name" { + Test-IsWebServer -ComputerName "ALK-DELL1234" | Should -BeFalse + } + } + + Context "Uses empty ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsWebServer -ComputerName "" | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsWebServer -ComputerName "" | Should -BeTrue + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsWebServer -ComputerName "" | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsWebServer -ComputerName "" | Should -BeFalse + } + } + + Context "Uses null ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsWebServer -ComputerName $null | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsWebServer -ComputerName $null | Should -BeTrue + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsWebServer -ComputerName $null | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsWebServer -ComputerName $null | Should -BeFalse + } + } + + Context "Uses no ComputerName parameter, delivers right result" { + It "Returns false if UUT has an app server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "APP123456"} + Test-IsWebServer | Should -BeFalse + } + + It "Returns true if UUT has a web server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "WEB123456"} + Test-IsWebServer | Should -BeTrue + } + + It "Returns false if UUT has a mic server name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "MIC123456"} + Test-IsWebServer | Should -BeFalse + } + + It "Returns false if UUT has a dell laptop name" { + Mock -ModuleName $moduleForMock -CommandName Get-FullyQualifiedServerName -MockWith { return "ALK-DELL1234"} + Test-IsWebServer | Should -BeFalse + } + } +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.ps1 new file mode 100644 index 0000000..6f9e081 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.ps1 @@ -0,0 +1,17 @@ +function Test-IsWindowsServer { + + <# + .SYNOPSIS + Returns a boolean indicating if the current machine's OS is a Windows Server operating system + + .DESCRIPTION + Queries CIM Instance Data to Identify if the Current Operating System is a Windows Server operating system. + Returns a boolean + #> + + [CmdletBinding()] + param() + + $osProductType = (Get-CimInstance -ClassName Win32_OperatingSystem -Property ProductType).ProductType + return $osProductType -in @(2, 3) +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.tests.ps1 new file mode 100644 index 0000000..ff10f9d --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServer.tests.ps1 @@ -0,0 +1,49 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsWindowsServer" { + + Context "CIM Results" { + + It "Returns True if The OS Product Type is 2" { + + Mock -CommandName Get-CimInstance -MockWith { + [PSCustomObject]@{ + ProductType = 2; + } + } -ModuleName $moduleForMock + + Test-IsWindowsServer | Should -BeTrue + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CimInstance -Scope It -Exactly 1 + } + + It "Returns True if The OS Product Type is 3" { + + Mock -CommandName Get-CimInstance -MockWith { + [PSCustomObject]@{ + ProductType = 3; + } + } -ModuleName $moduleForMock + + Test-IsWindowsServer | Should -BeTrue + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CimInstance -Scope It -Exactly 1 + } + + It "Returns False if The OS Product Type is 1" { + + Mock -CommandName Get-CimInstance -MockWith { + [PSCustomObject]@{ + ProductType = 1; + } + } -ModuleName $moduleForMock + + Test-IsWindowsServer | Should -BeFalse + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-CimInstance -Scope It -Exactly 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.ps1 new file mode 100644 index 0000000..50e1e72 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.ps1 @@ -0,0 +1,32 @@ +function Test-IsWindowsServerCore { + + <# + .SYNOPSIS + Returns a boolean indicating if the current machine's OS is a Windows Server Core operating system + + .DESCRIPTION + Queries the registry to identify if the current Operating System is a Windows Server Core operating system. + #> + + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + $result = $false + $logLead = (Get-LogLeadName) + + if ($false -eq (Test-IsWindowsServer)) { + + Write-Verbose "$logLead : Server is not a Windows Server; definitely not a Windows Server Core." + return $result + } + + $windowsVersion = Get-ItemProperty 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue + if ( $null -eq $windowsVersion ) { + + Write-Warning "$logLead : Unable to read Windows version from registry; assuming this is not Windows Server Core." + return $result + } + + return ( $windowsVersion.InstallationType -match 'Server Core' ) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.tests.ps1 new file mode 100644 index 0000000..878202b --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-IsWindowsServerCore.tests.ps1 @@ -0,0 +1,65 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IsWindowsServerCore" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "Test-IsWindowsServerCore.tests" } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Context "Results" { + + It "Returns False if Server is not Windows" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $false } + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { return $null } + + Test-IsWindowsServerCore | Should -BeFalse + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ItemProperty -Scope It -Exactly 0 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 0 + } + + It "Returns False if Unable to Read Windows Version Registry Setting" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { return $null } + + Test-IsWindowsServerCore | Should -BeFalse + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ItemProperty -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 1 ` + -ParameterFilter { $Message -match "Unable to read Windows version from registry" } + } + + It "Returns False if Windows Version Registry Setting Does Not Contain Core" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { return @{ InstallationType = 'Server' } } + + Test-IsWindowsServerCore | Should -BeFalse + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ItemProperty -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 0 + } + + It "Returns True if Windows Version Registry Setting Contains Core" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-ItemProperty -ModuleName $moduleForMock -MockWith { return @{ InstallationType = 'Server Core' } } + + Test-IsWindowsServerCore | Should -BeTrue + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-ItemProperty -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-ORBEnvironmentalVariables.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-ORBEnvironmentalVariables.ps1 new file mode 100644 index 0000000..dc6176e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-ORBEnvironmentalVariables.ps1 @@ -0,0 +1,41 @@ +function Test-ORBEnvironmentalVariables { +<# +.SYNOPSIS + Checks to verify that the expected environmental variables are present +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + $expectedVariables = @( + + @{ "Name" = "POD"; + "Description" = "This variable should be set to the name of the POD/Lane or Team Environment. The value must match other servers in the grouping exactly, including case."; + "ValidationRegex" = "[^\s]+"; + "ValidationError" = "The value for this variable cannot be an empty string" + }, + + @{ "Name" = "ServerRole"; + "Description" = "This variable should be set to either Web or App, depending on the function of the server"; + "ValidationRegex" = "^(Web|App)$"; + "ValidationError" = "The value for this variable must be either Web or App"; + } + ) + + foreach ($envVariable in $expectedVariables) { + $existingVariable = [Environment]::GetEnvironmentVariable($envVariable.Name, "Machine") + if ([String]::IsNullOrEmpty($existingVariable)) { + Write-Warning ("$logLead : The enviornmental variable {0} was not found. {1}" -f $envVariable.Name, $envVariable.Description) + + if ($null -ne $host) { + Set-ORBEnvironmentalVariables $envVariable + } + } + else { + Write-Output ("$logLead : Environmental variable {0} found with value {1}" -f $envVariable.Name, $existingVariable) + } + } +} + diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-OrbSymLinksExist.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-OrbSymLinksExist.ps1 new file mode 100644 index 0000000..c578097 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-OrbSymLinksExist.ps1 @@ -0,0 +1,30 @@ +function Test-OrbSymLinksExist { + <# + +.SYNOPSIS + Tests orb symlink folders exist, retrurns true of false +#> + [CmdletBinding()] + [OutputType([boolean])] + param() + $logLead = Get-LogLeadName + $appFolderNames = Get-OrbSymLinkFolderNames + $testPathFailingFolders = @() + $orbPath = Get-OrbPath + foreach ($appFolderName in $appFolderNames) { + + $testPath = Join-Path -Path $orbPath -ChildPath $appFolderName + $finalPath = "$testPath\bin\shared" + if ((Test-Path $finalPath) -eq $false) { + Write-Warning "$logLead : SymLink does not exist! : $finalpath" + $testPathFailingFolders += $appFolderNames + } + } + if ($testPathFailingFolders.count -ne 0) { + Write-Warning "$loglead : there are missing SymLink folders $($testPathFailingFolders -join ",")" + return $false + } else { + Write-Host "$loglead : found all SymLink folders $($testPathFailingFolders -join ",")" + return $true + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Test-RegistryKey.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Test-RegistryKey.ps1 new file mode 100644 index 0000000..60f87b2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Test-RegistryKey.ps1 @@ -0,0 +1,32 @@ +function Test-RegistryKey { +<# +.SYNOPSIS + Test if a registry key exists + +.DESCRIPTION + Tests if a specific Registry Key exists, not if a property of a key exists. + +.EXAMPLE + Test-RegistryKey -regKey HKCU:\Environment\foo -Verbose +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + $regKey + ) + + $logLead = Get-LogLeadName + + try { + Write-Verbose "$logLead : Testing for key's existence $regkey" + if (Test-Path -Path $regKey) { + Write-Verbose "$regKey exists." + } else { + throw "$logLead : Registry Key not found" + } + } catch { + return $false + } + + return $true +} diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowerShellModule.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowerShellModule.tests.ps1 new file mode 100644 index 0000000..9c866c8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowerShellModule.tests.ps1 @@ -0,0 +1,275 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Update-AWSPowerShellModule" { + + Import-Module PowerShellGet + + Mock Get-Module -ModuleName $moduleForMock -MockWith {} + Mock Install-Module -ModuleName $moduleForMock -MockWith {} + Mock Uninstall-Module -ModuleName $moduleForMock -MockWith {} + Mock Find-Module -ModuleName $moduleForMock -MockWith {} + Mock Test-Path -ModuleName $moduleForMock -MockWith {} + Mock Remove-FileSystemItem -ModuleName $moduleForMock -MockWith {} + + Mock Get-PSRepository -ModuleName $moduleForMock -MockWith {} + Mock Set-PSRepository -ModuleName $moduleForMock -MockWith {} + Mock Register-PSRepository -ModuleName $moduleForMock -MockWith {} + + Context "Error Handling" { + + It "Returns Early When No Remote Module Found" { + + Mock Write-Warning -ModuleName $moduleForMock -MockWith {} + Update-AWSPowerShellModule + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -ParameterFilter {$Message -match "Could not find module on remote repository" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-Module -Times 0 -Exactly -Scope It + } + + It "Returns Early When a Module Name that Would Cause Wildcard Matches is Passed" { + + Mock Write-Warning -ModuleName $moduleForMock -MockWith {} + Update-AWSPowerShellModule -ModuleName "AWS*" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -ParameterFilter {$Message -match "Wildcard module names are not allowed in this function" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-Module -Times 0 -Exactly -Scope It + } + + It "Writes a Warning and Uses the Highest Version Module For Comparison When Multiple Modules Matched" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + $localModuleOne = New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "8.8.8.8"; + Name = "AWSPowerShell"; + ExportedCommands = @("Submit-ResignationLetter") + } + + $localModuleTwo = New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "8.8.4.4"; + Name = "AWSPowerShell"; + ExportedCommands = @("Submit-ResignationLetter") + } + + return @($localModuleOne, $localModuleTwo) + } + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "8.8.4.4"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Mock Write-Warning -ModuleName $moduleForMock -MockWith {} + Update-AWSPowerShellModule + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -ParameterFilter {$Message -match "More than one module version found" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-Module -Times 1 -Exactly -Scope It + } + } + + Context "Function Logic" { + + It "Updates the PSRepository Source Location to the Value Provided via Parameter if it is Incorrect" { + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "9.9.9.9"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Mock Get-PSRepository -ModuleName $moduleForMark -MockWith { + + return New-Object PSObject -Property @{ + Name = "FakeOlFeed"; + InstallationPolicy = "Untrusted"; + SourceLocation = "https://stackoverflow.com/nuget/ThisShouldBeUpdatedByTheFunction"; + } + } + + $fakeFeedName = "FakeOlFeed" + $fakeFeed = "https://stackoverflow.com/nuget/PesterTest" + Update-AWSPowerShellModule -FeedName $fakeFeedName -SourceLocation $fakeFeed + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-PSRepository -ParameterFilter {$SourceLocation -eq $fakeFeed -and $Name -eq $fakeFeedName } + } + + It "Adds the PSRepository if it is Not Found" { + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "9.9.9.9"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Mock Get-PSRepository -ModuleName $moduleForMark -MockWith { + + return $null + } + + $fakeFeedName = "FakeOlFeed" + $fakeFeed = "https://stackoverflow.com/nuget/PesterTest" + Update-AWSPowerShellModule -FeedName $fakeFeedName -SourceLocation $fakeFeed + Assert-MockCalled -ModuleName $moduleForMock -CommandName Register-PSRepository -ParameterFilter {$SourceLocation -eq $fakeFeed -and $Name -eq $fakeFeedName } + } + + It "Exits Early if the Specified Version is Already Installed" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "9.0.9.0"; + Name = "AWSPowerShell"; + ExportedCommands = @("Format-HardDrive") + } + } + + Update-AWSPowerShellModule -TargetVersion "9.0.9.0" + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-Module -Times 0 -Exactly -Scope It + } + + It "Exits Early if No Version Specified and Latest Version is Already Installed" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "9.1.9.1"; + Name = "AWSPowerShell"; + ExportedCommands = @("Format-HardDrive") + } + } + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "9.1.9.1"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Update-AWSPowerShellModule + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-Module -Times 0 -Exactly -Scope It + } + + It "Uninstalls Current Version if Latest Remote Version Doesn't Match" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "9.2.9.2"; + Name = "AWSPowerShell"; + ExportedCommands = @("Format-HardDrive") + } + } + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "9.3.9.3"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Update-AWSPowerShellModule + Assert-MockCalled -ModuleName $moduleForMock -CommandName Uninstall-Module -Times 1 -Exactly -Scope It -ParameterFilter { $Name -eq "AWSPowerShell"} + } + + It "Removes the DeprecatedModuleFolder If it Exists" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "9.2.9.2"; + Name = "AWSPowerShell"; + ExportedCommands = @("Format-HardDrive") + } + } + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Version = "9.3.9.3"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + } + + Mock Test-Path -ModuleName $moduleForMock -MockWith { + + return $true + } + + $fakePath = "Z:\Temp\PesterTestCase" + Update-AWSPowerShellModule -DeprecatedModuleFolder $fakePath + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-Path -Times 1 -Exactly -Scope It -ParameterFilter { $Path -eq $fakePath } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Remove-FileSystemItem -Times 1 -Exactly -Scope It -ParameterFilter { $Path -eq $fakePath } + } + + It "Uses the Module Name From the Parameters" { + + Mock Get-Module -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ModuleType = "Binary"; + Version = "9.4.9.4"; + Name = "BogusModule"; + ExportedCommands = @("Submit-ResignationLetter") + } + } + + Mock Find-Module -ModuleName $moduleForMock -MockWith { + + $fakeLocalModule1 = New-Object PSObject -Property @{ + Version = "9.5.9.5"; + Name = "AWSPowerShell"; + Repository = "FakeOlFeed"; + Description = "This is Fake Homie"; + } + + $fakeLocalModule2 = New-Object PSObject -Property @{ + Versoin = "9.6.9.6"; + Name = "BogusModule"; + Repository = "FakeOlFeed"; + Description = "This is also fake, homeslice"; + } + + return @( + $fakeLocalModule1, + $fakeLocalModule2 + ) + } + + $fakeModuleName = "BogusModule" + Update-AWSPowerShellModule -ModuleName $fakeModuleName + Assert-MockCalled -ModuleName $moduleForMock -CommandName Find-Module -Times 1 -Exactly -Scope It -ParameterFilter { $Name -eq $fakeModuleName} + Assert-MockCalled -ModuleName $moduleForMock -CommandName Uninstall-Module -Times 1 -Exactly -Scope It -ParameterFilter { $Name -eq $fakeModuleName} + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-Module -Times 1 -Exactly -Scope It -ParameterFilter { $Name -eq $fakeModuleName} + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowershellModule.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowershellModule.ps1 new file mode 100644 index 0000000..7ddc816 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Update-AWSPowershellModule.ps1 @@ -0,0 +1,168 @@ +function Update-AWSPowerShellModule { +<# + +.SYNOPSIS + Updates the AWS Powershell module from a PSGet feed + +.DESCRIPTION + Configures parameter driven PSGet feed as a PS Repository to download from and install AWSPowerShell via Install-Module + +.PARAMETER SourceLocation + [string] The nuget URI to a PSRepository feed. Defaults to https://packagerepo.orb.alkamitech.com/nuget/SRETools + +.PARAMETER FeedName + [string] The name of the PSRepository feed to be registered. Defaults to SRETools + +.PARAMETER ModuleName + [string] The module name to update. Defaults to AWSPowerShell + +.PARAMETER DeprecatedModuleFolder + [string] The full path for any deprecated folders which should be removed as part of uninstall. Defaults to C:\Program Files (x86)\AWS Tools\PowerShell + +.PARAMETER TargetVersion + [string] When specified, targets a specific remote version of the specified module. When unspecified, the latest remote version is used. Defaults to null. + +.EXAMPLE + Update-AWSPowershellModule + + [Update-AWSPowershellModule] : Current Available AWSPowerShell Module Version: 4.1.14.0 + [Update-AWSPowershellModule] : Repository SRETools already configured correctly + [Update-AWSPowershellModule] : Looking for Available Remote Versions + [Update-AWSPowershellModule] : No newer version of AWSPowerShell available. + +.EXAMPLE + Update-AWSPowershellModule -TargetVersion 4.1.14.0 + + [Update-AWSPowershellModule] : Current Available AWSPowerShell Module Version: 4.1.10.0 + [Update-AWSPowershellModule] : Repository SRETools already configured correctly + [Update-AWSPowershellModule] : Looking for Remote Version 4.1.14.0 + [Update-AWSPowershellModule] : Uninstalling Module AWSPowerShell version 4.1.10.0 + [Update-AWSPowershellModule] : Installing Module AWSPowerShell version 4.1.14.0. This may take a few minutes. + [Update-AWSPowershellModule] : Module AWSPowerShell Version 4.1.14.0 installed successfully +#> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$SourceLocation = "https://packagerepo.orb.alkamitech.com/nuget/SRETools", + + [Parameter(Mandatory = $false)] + [string]$FeedName = "SRETools", + + [Parameter(Mandatory = $false)] + [string]$ModuleName = "AWSPowerShell", + + [Parameter(Mandatory = $false)] + [string]$DeprecatedModuleFolder = "C:\Program Files (x86)\AWS Tools\PowerShell", + + [Parameter(Mandatory = $false)] + [string]$TargetVersion = $null + ) + + $logLead = (Get-LogLeadName) + + if ($ModuleName -match "\*") { + + Write-Warning "$logLead : Wildcard module names are not allowed in this function. Remove any wildcards and re-execute" + return + } + + $currentModule = Get-Module -ListAvailable -Name $ModuleName + + if ($currentModule.Count -gt 1) { + + $firstMatchingModule = $currentModule | Sort-Object -Property Version -Descending | Select-Object -First 1 + Write-Warning "$logLead : More than one module version found with name $ModuleName. Installation will continue comparing against version $($firstMatchingModule.Version)." + Write-Warning "$logLead : All installed module versions will be removed as part of installation, if determined to be necessary based on this comparison version." + $currentModule = $firstMatchingModule + } + + if ($null -ne $currentModule) { + + if (($null -ne (Get-Module -Name $ModuleName)) -or ($null -ne ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {$_.Location -match $ModuleName}))) { + + Write-Host "$logLead : Module $ModuleName Loaded In Context. Force removing..." + Remove-Module $ModuleName -Force -ErrorAction SilentlyContinue + + if ($null -ne ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {$_.Location -match $ModuleName})) { + + Write-Warning "$logLead : $ModuleName assemblies are still loaded in the host. If installation fails, you must run this command from a fresh runspace." + Write-Host "$logLead : Execute this command to see the loaded location:`n`n`t[System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {`$_.Location -match `"$ModuleName`"}" + } + } + + Write-Host "$logLead : Current Available $ModuleName Module Version: $($currentModule.Version)" + + if ((-Not [String]::IsNullOrEmpty($TargetVersion)) -and ($currentModule.Version.ToString() -match $TargetVersion)) { + + Write-Host "$logLead : Version from arguments ($TargetVersion) matches local module version ($($currentModule.Version)). Exiting" + return + } + } else { + + Write-Host "$logLead : Module $ModuleName is not Installed" + } + + $currentRepo = Get-PSRepository -Name $FeedName -ErrorAction SilentlyContinue + if ($null -eq $currentRepo) { + + Write-Host "$logLead : Registering Feed Name: $FeedName" + Register-PSRepository -Name $FeedName -SourceLocation $SourceLocation + } elseif ($currentRepo.SourceLocation -ne $SourceLocation) { + + Write-Host "$logLead : Correcting Feed URL for Feed Name: $FeedName from $($currentRepo.SourceLocation) to $SourceLocation" + Set-PSRepository -Name $FeedName -SourceLocation $SourceLocation + } else { + + Write-Host "$logLead : Repository $FeedName already configured correctly" + } + + if ([String]::IsNullOrEmpty($TargetVersion)) { + + Write-Host "$logLead : Looking for Available Remote Versions" + $latestRemoteModule = Find-Module -Name $ModuleName -Repository $FeedName -ErrorAction SilentlyContinue + } else { + + Write-Host "$logLead : Looking for Remote Version $TargetVersion" + $latestRemoteModule = Find-Module -Name $ModuleName -Repository $FeedName -RequiredVersion $TargetVersion -ErrorAction SilentlyContinue + } + + if ($null -eq $latestRemoteModule) { + + Write-Warning "$logLead : Could not find module on remote repository. Verify PSRepository configuration and rerun" + return + } elseif ($null -ne $currentModule -and ($currentModule.Version.ToString() -contains $latestRemoteModule.Version)) { + + Write-Host "$logLead : No newer version of $ModuleName available." + return + } elseif ($null -ne $currentModule) { + + Write-Host "$logLead : Uninstalling All Versions of Local Module $ModuleName" + Uninstall-Module -Name $ModuleName -AllVersions + + if (Test-Path $DeprecatedModuleFolder) { + + Write-Host "$logLead : Removing Old Module Folder: $DeprecatedModuleFolder" + Remove-FileSystemItem -Path $DeprecatedModuleFolder -Recurse -Force + } + } + + if ([String]::IsNullOrEmpty($TargetVersion)) { + + Write-Host "$logLead : Installing Module $ModuleName version $($latestRemoteModule.Version). This may take a few minutes." + Install-Module -Name $ModuleName -Repository $FeedName -Force -SkipPublisherCheck + } else { + + Write-Host "$logLead : Installing Module $ModuleName version $TargetVersion. This may take a few minutes." + Install-Module -Name $ModuleName -Repository $FeedName -Force -SkipPublisherCheck -RequiredVersion $TargetVersion + } + + $newModule = Get-Module -ListAvailable -Name $ModuleName + if ($null -eq $newModule -or $newModule.Version.ToString() -notmatch $latestRemoteModule.Version) { + + Write-Warning "$logLead : Installation Unsuccessful. Review logs for errors." + } else { + + Write-Host "$logLead : Module $ModuleName Version $($newModule.Version) installed successfully" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.ps1 new file mode 100644 index 0000000..981df89 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.ps1 @@ -0,0 +1,534 @@ +function Update-SystemPortReservations { +<# +.SYNOPSIS + Updates the system port reservations if they haven't been set yet. + This script is intended to automate me out of a job. This script is for developer environment setup and exceptional override + +.PARAMETER AddRange + Supply an array of value-tuples for creating one or more ranges. + Value tuple is defined as an array of two integers: StartPort, NumerOfPorts + Example input: Update-SystemPortReservations -AddRange @(50000,30) + Example input: Update-SystemPortReservations -AddRange @(@(50000,30),@(12345,2)) + +.PARAMETER RemoveRange + Supply an array of value-tuples for removing one or more ranges. + Value tuple is defined as an array of two integers: StartPort, NumerOfPorts + Example input: Update-SystemPortReservations -RemoveRange @(@(50000,30),@(12345,2)) +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias("Create")] + $AddRange = $null, + [Parameter(Mandatory = $false)] + [Alias("Destroy")] + $RemoveRange = $null + ) + + $logLead = (Get-LogLeadName) + $validationErrors = @() + $wasRunningProcessNames = @() + + Write-Host "$logLead : Ensure basic system registrations have occurred - IPListens" + Set-DefaultNetshIPListens + + Write-Host "$logLead : Ensure basic system registrations have occurred - URL ACLs" + Set-DefaultNetshURLACLS + + #region internal-private functions to make life easier + function Get-EnvironmentVariableNameForRange { + <# + .SYNOPSIS + This function is internal only and is used to consistently return the variable name + This function should not be a standalone function as it only applies to this specific file + + .PARAMETER rangeStart + This parameter is the Start of the range. + + .PARAMETER NumberOfPorts + The number of ports in the range + + .PARAMETER End + The End port in the range + #> + [CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')] + param ( + [Parameter(Mandatory = $false)] + $Start, + [Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')] + [ValidateNotNullOrEmpty()] + [Alias("Range")] + [int]$NumberOfPorts, + [Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')] + [ValidateNotNullOrEmpty()] + [Alias("EndPort")] + [int]$End + ) + + if ($Start -le 0) { + throw "$logLead : Start port value must be greater than 0" + } + + if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') { + if ($NumberOfPorts -eq $Start) { + throw "$logLead : NumberOfPorts can not equal the parameter for Start" + } + + if ($NumberOfPorts -le 0) { + throw "$logLead : NumberOfPorts value must be greater than 0" + } + + $End = $NumberOfPorts + $Start - 1 + } + + if ($PSCmdlet.ParameterSetName -eq 'EndPorts') { + if ($End -le $Start) { + throw "$logLead : End port value must be larger than Start port value" + } + + if ($End -le 0) { + throw "$logLead : End port value must be greater than 0" + } + } + + return "ALKAMI.SRE.EXCLUDED_PORT_RANGE_CONFIGURED.$Start.$End" + } + + function Stop-AnyProcessesInPortRange { + <# + .SYNOPSIS + Stop any processes that are using these ports. Return the names so we can try to restart any services. + + .PARAMETER Start + This parameter is the Start of the range. + + .PARAMETER NumberOfPorts + The number of ports in the range + + .PARAMETER End + The End port in the range + #> + [CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')] + param ( + [Parameter(Mandatory = $false)] + $Start, + [Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')] + [ValidateNotNullOrEmpty()] + [Alias("Range")] + [int]$NumberOfPorts, + [Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')] + [ValidateNotNullOrEmpty()] + [Alias("EndPort")] + [int]$End + ) + + $runningProcessNames = @() + + if ($Start -le 0) { + throw "$logLead : Start port value must be greater than 0" + } + + if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') { + if ($NumberOfPorts -eq $Start) { + throw "$logLead : NumberOfPorts can not equal the parameter for Start" + } + + if ($NumberOfPorts -le 0) { + throw "$logLead : NumberOfPorts value must be greater than 0" + } + + $End = $NumberOfPorts + $Start - 1 + } + + if ($PSCmdlet.ParameterSetName -eq 'EndPorts') { + if ($End -le $Start) { + throw "$logLead : End port value must be larger than Start port value" + } + + if ($End -le 0) { + throw "$logLead : End port value must be greater than 0" + } + } + + $allBoundProcessPorts = (Get-NetTCPConnection).LocalPort | Sort-Object -Unique + + Write-Host "$logLead : Looking for ports between [$Start] and [$End]" + + $runningProcessPorts = @($allBoundProcessPorts.Where({$_ -ge $Start -and $_ -le $End})) + + if (Test-IsCollectionNullOrEmpty $runningProcessPorts) { + Write-Host "$logLead : Found no actively used ports, nothing to stop" + return $runningProcessNames + } + + Write-Host "$logLead : Found [$($runningProcessPorts.Length)] actively used ports" + + # We found some processes, let's kill 'em + foreach($processPort in $runningProcessPorts) { + # It's possible we already killed a process that was holding more than one port open + $netTcpConnection = Get-NetTCPConnection -LocalPort $processPort -ErrorAction Ignore + if ($null -ne $netTcpConnection) { + $process = Get-Process -Id ($netTcpConnection).OwningProcess -ErrorAction Ignore + + if ($null -eq $process) { + # The process was already killed a moment ago. This is normal. + continue + } + + $processName = $process.Name + $processPath = $process.Path + + # Test to ensure this is not the System process, and that it has a path + if ([string]::IsNullOrEmpty($processPath)) { + Write-Host "$logLead : [$processName] was running on process [$processPort] with no path. Skipping" + continue + } + + Write-Warning "$logLead : Found [$processName] running and keeping [$processPort] open. Killing this process by force." + Write-Host "$logLead : [$processName] was running at path [$processPath]. Will try to restart at the end if this was a service" + $runningProcessNames += $processName + + $processId = $process.Id + Stop-Process -Id $processId -Force + } + } + + return $runningProcessNames + } + #endregion internal-private functions to make life easier + + Write-Host "$logLead : Updating system port reservations" + + <# + Once a range is committed to this table, please do not edit it except under extreme duress + If you need to add "a longer port" you would want to just make a new grouping + Example: the "standard default" is 50000 + 30 ports (to 50029) so if we wanted instead + 50000 + 60, what you would do here is @{ Start = 50030; NumberOfPorts = 30; } + Plus the other fields as indicated below. + + These ranges once set are "set in stone" + There are machine level environment variables that get set as well + So if you want to remove something, you have to ensure you call that + #> + $createRanges = @( + @{ Start = 50000; NumberOfPorts = 30; CollidingReservations = @(); } + @{ Start = 12345; NumberOfPorts = 2; CollidingReservations = @(); } + ) + + # Be very careful what you do here. + # If you MUST delete one from the above table, move it here. + $destroyExistingRanges = @( + ) + + #region process the inputs + if (!(Test-IsCollectionNullOrEmpty $AddRange)) { + # $AddRange was supplied with some set of values. Let's evaluate those for validness. + $createRanges = @() + if (($AddRange.Length -eq 2) -and ($AddRange[0] -is [int])) { + $createRanges += @{ Start = $AddRange[0]; NumberOfPorts = $AddRange[1]; CollidingReservations = @(); } + } else { + foreach($range in $AddRange) { + if (($range.Length -eq 2) -and ($range[0] -is [int]) -and ($range[1] -is [int])) { + $createRanges += @{ Start = $range[0]; NumberOfPorts = $range[1]; CollidingReservations = @(); } + } else { + $validationErrors += "$logLead : Could not parse the input parameter for AddRange. Should be an array of two ints, or an array of arrays of int pairs." + } + } + } + } elseif (!(Test-IsCollectionNullOrEmpty $RemoveRange)) { + # There was no create range input, but there was a remove range input + # Therefore we only want to remove the ranges supplied + $createRanges = @() + } + + if (!(Test-IsCollectionNullOrEmpty $RemoveRange)) { + # $RemoveRange was supplied with some set of values. Let's evaluate those for validness. + $destroyExistingRanges = @() + if (($RemoveRange.Length -eq 2) -and ($RemoveRange[0] -is [int])) { + $destroyExistingRanges += @{ Start = $RemoveRange[0]; NumberOfPorts = $RemoveRange[1]; } + } else { + foreach($range in $RemoveRange) { + if (($range.Length -eq 2) -and ($range[0] -is [int]) -and ($range[1] -is [int])) { + $destroyExistingRanges += @{ Start = $range[0]; NumberOfPorts = $range[1]; } + } else { + $validationErrors += "$logLead : Could not parse the input parameter for RemoveRange. Should be an array of two ints, or an array of arrays of int pairs." + } + } + } + } + + if (!(Test-IsCollectionNullOrEmpty $validationErrors)) { + #Some errors were found. Stop now. + + foreach($validationError in $validationErrors) { + Write-Error $validationError + } + throw "$logLead : Please resolve errors in input and try again" + } + #endregion process the inputs + + #region fast abort if create-only and all ranges exist + # Start with true only if we have values to test, otherwise start with false + $allRangeMachineEnvVarsExist = !(Test-IsCollectionNullOrEmpty $createRanges) + + if (Test-IsCollectionNullOrEmpty $destroyExistingRanges) { + foreach($range in $createRanges) { + if ($null -eq (Get-EnvironmentVariable -Name (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) -StoreName Machine)) { + # $null means the var does not exist + $allRangeMachineEnvVarsExist = $false + break + } + } + } + + if ($allRangeMachineEnvVarsExist) { + Write-Host "$logLead : All the requested ranges seem to exist. Nothing to do." + return + } + #endregion fast abort if create-only and all ranges exist + +<# + __ __ __ ____ _ _ +( \/ ) /__\ (_ _)( )_( ) + ) ( /(__)\ )( ) _ ( +(_/\/\_)(__)(__)(__) (_) (_) +,_ . . +|_ ,-. | | ,-. . , , ,-. +| | | | | | | |/|/ `-. +| `-' `' `' `-' ' ' `-' +' + +There are two locations below that use this set of calculations. +I included this enormous block of text so that I can keep track of the +Logical math taking place. There's 3 complex logic sets to monitor +So I want to make sure that my logic is sound + +It is possible to have overlapping ranges that are problematic +We need to remove some conditions if they already exist +In some cases they may be our own cause, but hopefully we already +Resolved those with the removals above. + +In the examples below we want to consider a $range in $createRanges +Range: Start 100 NumberOfPorts 100 +DesiredRangeStart: 100 (DRS) +DesiredRangeEnd: 199 (DRE) + +Non-conflicting conditions to handle: +Defined range matches our request +How can this happen? This script runs on a machine that has +already been configured before the environment variables were added +RangeStart: 100 (RS) +RangeEnd: 199 (RE) +RS -eq DRS -and RE -eq DRE + +Action to be taken in this case: +Create the environment variable on this machine, continue loop + +Conflicting conditions: + +Conflict #1: "hangs over the beginning" +99 - 101 already exists as a reserved set of ports +RangeStart: 99 (RS) +RangeEnd: 101 (RE) +RS -lt DRS -and RE -gt DRS -and RE -lt DRE +99 -lt 100 -and 101 -gt 100 -and 101 -lt 199 + +Conflict #2: "hangs over the end" +198 - 201 already exists as a reserved set of ports +RangeStart: 198 (RS) +RangeEnd: 201 (RE) +RS -gt DRS -and RS -lt DRE -and RE -gt DRE +198 -gt 100 -and 198 -lt 199 -and 201 -gt 199 + +Conflict #3: "narrow condition" +140 - 160 already exists as a reserved set of ports +RangeStart: 140 (RS) +RangeEnd: 160 (RE) +RS -gt DRS -and RE -lt DRE +140 -gt 100 -and 160 -lt 199 + +As a visualization: +Desired: |--------------------| +Conf 1: |-----| +Conf 2: |-----| +Conf 3: |------| +#> + + #region validate the create ranges don't overlap + foreach($range in $createRanges) { + $desiredRangeStart = $range.Start + $desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1 + $otherRanges = $createRanges.Where({!(($_.Start -eq $range.Start) -and ($_.NumberOfPorts -eq $range.NumberOfPorts))}) + foreach($otherRange in $otherRanges) { + $rangeStart = $otherRange.Start + $rangeEnd = $otherRange.Start + $otherRange.NumberOfPorts - 1 + + # As a visualization: + # Desired: |--------------------| + # Conf 1: |-----| + # Conf 2: |-----| + # Conf 3: |------| + + $conflictCondition1 = ($rangeStart -le $desiredRangeStart) -and ($rangeEnd -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd) + $conflictCondition2 = ($rangeStart -gt $desiredRangeStart) -and ($rangeStart -lt $desiredRangeEnd) -and ($rangeEnd -ge $desiredRangeEnd) + $conflictCondition3 = ($rangeStart -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd) + + if ($conflictCondition1 -or $conflictCondition2 -or $conflictCondition3) { + $validationErrors += "$logLead : Range crossover exception. Create range with Start $($range.Start) and NumberOfPorts $($range.NumberOfPorts) has a range conflict with the create range with values Start $($otherRange.Start) NumberOfPorts $($otherRange.NumberOfPorts)." + } + } + } + + if (!(Test-IsCollectionNullOrEmpty $validationErrors)) { + #Some errors were found. Stop now. + + foreach($validationError in $validationErrors) { + Write-Error $validationError + } + throw "$logLead : Please resolve errors in input and try again" + } + #endregion validate the ranges don't overlap + + $existingRanges = @(Get-NetshExcludedPortRanges) + + #region ensure the delete ranges already exist to be deleted + foreach($range in $destroyExistingRanges) { + $desiredRangeStart = $range.Start + $desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1 + $activeDeleteRanges = $existingRanges.Where({!(($_.Start -eq $desiredRangeStart) -and ($_.End -eq $desiredRangeEnd))}) + if (Test-IsCollectionNullOrEmpty $activeDeleteRanges) { + $validationErrors += "$logLead : There are no existing ranges to remove for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts) FinalPort $($desiredRangeEnd)" + } + } + + if (!(Test-IsCollectionNullOrEmpty $validationErrors)) { + #Some errors were found. Stop now. + + foreach($validationError in $validationErrors) { + Write-Error $validationError + } + throw "$logLead : Please resolve errors in input and try again" + } + #endregion ensure the delete ranges already exist to be deleted + + #region destroy before creating + if (!(Test-IsCollectionNullOrEmpty $destroyExistingRanges)) { + # Destroying before you create saves you a lot of hassle + # It does mean reading the system state twice, but that should be ok for what we are doing + foreach($range in $destroyExistingRanges) { + $wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) + + Write-Host "$logLead : Ready to delete range with Start $($range.Start) NumberOfPorts $($range.NumberOfPorts)" + + # The function returns a true-false value if it succeeds or fails + if (Remove-NetshExcludedPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) { + Remove-EnvironmentVariable -Name (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) -StoreName Machine + } + } + + if (Test-IsCollectionNullOrEmpty $createRanges) { + # If we only came here to destroy things, let's get out of here! + Write-Host "$logLead : All ranges destroyed, nothing to create. Done" + return + } + + # Since we just nuked the ranges, let's just recreate from the system to be sure we got 'em all + $existingRanges = (Get-NetshExcludedPortRanges) + } + #endregion destroy before creating + + #region look for existing non-reserved ranges that need to be destroyed + foreach($range in $createRanges) { + $desiredRangeStart = $range.Start + $desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1 + $envVarName = (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) + if ($null -ne (Get-EnvironmentVariable $envVarName)) { + Write-Verbose "$logLead : non-reserved destroy range check : Environment range already set for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts), skipping" + continue + } + foreach($existingRange in $existingRanges) { + $rangeStart = $existingRange.Start + $rangeEnd = $existingRange.End + + # As a visualization: + # Desired: |--------------------| + # Non-Conf: |--------------------| + # Conf 1: |-----| + # Conf 2: |-----| + # Conf 3: |------| + + $nonconflictCondition = ($rangeStart -eq $desiredRangeStart) -and ($rangeEnd -eq $desiredRangeEnd) + + if ($nonconflictCondition) { + Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine + Write-Host "$logLead : Found an already existing range for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts). Setting Machine Environment Variable and continuing." + continue + } + + $conflictCondition1 = ($rangeStart -le $desiredRangeStart) -and ($rangeEnd -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd) + $conflictCondition2 = ($rangeStart -gt $desiredRangeStart) -and ($rangeStart -lt $desiredRangeEnd) -and ($rangeEnd -ge $desiredRangeEnd) + $conflictCondition3 = ($rangeStart -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd) + + if ($conflictCondition1 -or $conflictCondition2 -or $conflictCondition3) { + $range.CollidingReservations += @{ Start = $rangeStart; End = $rangeEnd; } + } + } + } + #endregion look for existing non-reserved ranges that need to be destroyed + + #region remove excess regions so we can create the ones we need to create + $haveDeleteRanges = @($createRanges.Where({!(Test-IsCollectionNullOrEmpty $_.CollidingReservations)})) + if (!(Test-IsCollectionNullOrEmpty $haveDeleteRanges)) { + $allBoundProcessPorts = (Get-NetTCPConnection).LocalPort | Sort-Object -Unique + foreach($range in $haveDeleteRanges) { + foreach ($reservation in $range.CollidingReservations) { + # Remove the existing reservations + # There may be a port conflict because something may have the port in use. + # Now we get to see if ANY other application is using that port right now, + # And try to force-kill them + # We can't clear the port range until these processes are stopped. + # We have to repeat the process before we create each range, as well, just in case. + + $wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $reservation.Start -End $reservation.End) + + $portCount = $reservation.End - $reservation.Start + 1 + # TODO: Add support for SupportsShouldProcess + Write-Host "$logLead : Removing conflicting port reservation to enable properly configuring requested range. Removing Start [$($reservation.Start)] NumberOfPorts [$($portCount)]" + # The function returns a true-false value if it succeeds or fails + if (Remove-NetshExcludedPortRange -Start $reservation.Start -NumberOfPorts $portCount) { + Remove-EnvironmentVariable (Get-EnvironmentVariableNameForRange -Start $reservation.Start -NumberOfPorts $portCount) + } + } + } + } + #endregion remove excess regions so we can create the ones we need to create + + #region now the moment we've all been waiting for + # all the overlapping/conflicting ranges should now be deleted + foreach($range in $createRanges) { + $envVarName = (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) + if ($null -ne (Get-EnvironmentVariable $envVarName)) { + Write-Host "$logLead : Environment range already set for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts), skipping" + continue + } + + # Ensure that the ports we want to create don't have anything using them + # This is usually where things go hairy if at all + $wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) + + # The function returns a true-false value if it succeeds or fails + if (Add-NetshExcludedPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) { + Write-Host "$logLead : Created excluded port range with Start $($range.Start) NumberOfPorts $($range.NumberOfPorts)" + Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine + } + } + #endregion now the moment we've all been waiting for + + #region try to restart services once. If it doesn't work, give up + foreach($processName in $wasRunningProcessNames) { + Write-Host "$logLead : We stopped [$processName] to do the port configuration, trying to start it if it was a service" + # Start-AlkamiService doesn't care if it wasn't actually a service if we SilentlyContinue + Start-AlkamiService -ServiceName $processName -ErrorAction SilentlyContinue + } + #endregion try to restart services once. If it doesn't work, give up +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.tests.ps1 b/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.tests.ps1 new file mode 100644 index 0000000..a175350 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.tests.ps1 @@ -0,0 +1,88 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Update-SystemPortReservations +Describe "Update-SystemPortReservations" -Tags @("Integration") { + Mock -ModuleName $moduleForMock Remove-NetshExcludedPortRange { Write-Host "removed 1" } -Verifiable + Mock -ModuleName $moduleForMock Add-NetshExcludedPortRange { Write-Host "created 1" } -Verifiable + + Mock -ModuleName $moduleForMock Get-NetshExcludedPortRanges { return @(@{ Start = 1; End = 10; }); } -Verifiable + Mock -ModuleName $moduleForMock Set-DefaultNetshIPListens {} -Verifiable + Mock -ModuleName $moduleForMock Set-DefaultNetshURLACLS {} -Verifiable + Mock -ModuleName $moduleForMock Start-AlkamiService {} -Verifiable + + Mock -ModuleName $moduleForMock Set-EnvironmentVariable { } -Verifiable + Mock -ModuleName $moduleForMock Get-EnvironmentVariable { return $null } -Verifiable + Mock -ModuleName $moduleForMock Remove-EnvironmentVariable { } -Verifiable + + Mock -ModuleName $moduleForMock Get-LogLeadName { "SUT: Update-SystemPortReservations" } + + Mock -ModuleName $moduleForMock Get-NetTCPConnection { Write-Host "asked for a connection"; return $null } -Verifiable + Mock -ModuleName $moduleForMock Get-Process { return $null } + Mock -ModuleName $moduleForMock Stop-Process { } + Mock -ModuleName $moduleForMock Write-Error { } + Mock -ModuleName $moduleForMock Write-Verbose { } + Mock -ModuleName $moduleForMock Write-Warning { } + + It "handles a basic create pair" { + Update-SystemPortReservations -Create @(50000,60) + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 1 -CommandName Add-NetshExcludedPortRange + } + + It "handles a basic remove pair with no creates" { + Update-SystemPortReservations -Remove @(1,11) + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 1 -CommandName Remove-NetshExcludedPortRange + } + + It "throws when there is no port to remove as asked" { + { Update-SystemPortReservations -Remove @(1,10) } | Should -Throw + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + It "throws with a basic remove pair where one port doesn't exist" { + { Update-SystemPortReservations -Remove @(1,10),@(20,5) } | Should -Throw + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + It "creates two ports with the default configuration" { + Update-SystemPortReservations + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 2 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + It "creates one port because one already existed" { + Update-SystemPortReservations -Create @(50000,60),@(1,10) + # This is actually _wrong_ but because we aren't managing a super complex system state above, we assume two got created + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 2 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + # Seriously, don't do this in the real world + It "doesn't handle removing and adding the same port" { + { Update-SystemPortReservations -Create @(1,10) -Remove @(1,10) } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + It "handles the same addition twice" { + Update-SystemPortReservations -Create @(50000,60),@(50000,60) + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 1 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } + + It "can't handle overlapping create inputs" { + { Update-SystemPortReservations -Create @(50000,60),@(50010,60) } | Should -Throw + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Add-NetshExcludedPortRange + Assert-MockCalled -ModuleName $moduleForMock -Scope It -Times 0 -CommandName Remove-NetshExcludedPortRange + } +} + +#endregion Update-SystemPortReservations diff --git a/Modules/Alkami.PowerShell.Configuration/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.Configuration/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Configuration/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.Configuration/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.Configuration/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.nuspec b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.nuspec new file mode 100644 index 0000000..f6b1252 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.nuspec @@ -0,0 +1,32 @@ + + + + Alkami.PowerShell.Database + $version$ + Alkami Platform Modules - PowerShell - Database + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.Database + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Database module for use with PowerShell. + + PowerShell + Copyright (c) 2019 Alkami Technologies + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.psd1 b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.psd1 new file mode 100644 index 0000000..4deb2d2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.psd1 @@ -0,0 +1,12 @@ +@{ + RootModule = 'Alkami.PowerShell.Database.psm1' + ModuleVersion = '1.5.0' + GUID = 'fba000f5-4576-40e8-aab9-71e5239ed876' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2019 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing developer and SDK specific operations.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Assert-SqlQueryIsSafe','Confirm-DatabaseAccess','ConvertTo-WhatIfQuery','Get-AllAWSEnvironmentDataTenants','Get-DatabaseInstallPath','Get-DeveloperTenant','Get-FormattedConnectionString','Get-FullTenantListFromServer','Get-MigrationRunnerExe','Get-MigrationRunnerPath','Get-ProvidersFromTenantDatabase','Get-ReportServerCredentialsFromMaster','Import-DeveloperDynamicTenant','Import-TenantsToServer','Initialize-AlkamiDatabase','Invoke-AlkamiLegacyOrbMigrations','Invoke-AlkamiMigrationRunner','Invoke-ExecuteQueryByConnectionString','Invoke-Migrate','Invoke-NonQueryByConnectionString','Split-ConnectionString','Test-DatabaseExists','Test-TenantConfiguration' +} diff --git a/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.pssproj b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.pssproj new file mode 100644 index 0000000..cf2fd59 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Alkami.PowerShell.Database.pssproj @@ -0,0 +1,57 @@ + + + Debug + 2.0 + {fba000f5-4576-40e8-aab9-71e5239ed876} + Exe + MyApplication + MyApplication + Alkami.PowerShell.Database + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.Database") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Database/AlkamiManifest.xml new file mode 100644 index 0000000..a4f7b77 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.Database + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.Database/Public/Assert-SqlQueryIsSafe.ps1 b/Modules/Alkami.PowerShell.Database/Public/Assert-SqlQueryIsSafe.ps1 new file mode 100644 index 0000000..182383b --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Assert-SqlQueryIsSafe.ps1 @@ -0,0 +1,102 @@ +function Assert-SqlQueryIsSafe { +<# +.SYNOPSIS + This function will verify that a Sql Query does not violate the expected Alkami key phrases that can not be run via this script. + Queries that violate this will need to be authorized or run in a different manner. + +.PARAMETER QueryString + The query to be validated + +.OUTPUTS + Returns true or false, and writes errors to the error stream (can cause the function to early-exit based on ErrorPreference) +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('Sql')] + [Alias('Query')] + [string]$QueryString + ) + + $logLead = Get-LogLeadName + + [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.SqlParser") | Out-Null + $parseOptions = New-Object Microsoft.SqlServer.Management.SqlParser.Parser.ParseOptions + + $restrictedPhrases = @( + 'DROP FULLTEXT INDEX ON' + 'DROP PROCEDURE' + 'DROP TABLE' + 'DROP VIEW' + 'DROP INDEX' + 'DROP TRIGGER' + 'DROP XML SCHEMA COLLECTION' + 'DROP TYPE' + 'CREATE FULLTEXT INDEX ON TABLE' + 'CREATE FUNCTION' + 'CREATE PROCEDURE' + 'CREATE TABLE' + 'CREATE VIEW' + 'CREATE TRIGGER' + 'CREATE XML SCHEMA COLLECTION' + 'CREATE TYPE' + 'CREATE UNIQUE CLUSTERED INDEX' + 'CREATE UNIQUE NONCLUSTERED INDEX' + 'CREATE UNIQUE INDEX' + 'CREATE CLUSTERED INDEX' + 'CREATE NONCLUSTERED INDEX' + 'CREATE INDEX' + 'CREATE PRIMARY XML INDEX' + 'CREATE XML INDEX' + 'ALTER FUNCTION' + 'ALTER PROCEDURE' + 'ALTER TABLE' + 'ALTER VIEW' + ) + + $parsedScript = [Microsoft.SqlServer.Management.SqlParser.Parser.Parser]::Parse($Query, $parseOptions) + + if (-not (Test-IsCollectionNullOrEmpty -Collection $parsedScript.Errors)) { + Write-Error "$logLead : Script is invalid. See errors: `n$($script.Errors.Message -join "`n")" + return $false + } + + # convert the query to a lexed string then join with spaces so we can do regex magic below + $parsedTokens = @($parsedScript.Script.Tokens.Type) + $parsedTokensAsString = $parsedTokens -join ' ' + + $errors = @() + + foreach ($phrase in $restrictedPhrases) { + # convert each phrase into a lexed string, then rejoin with spaces, and use the power of regex to count the number of occurrences + $parsedPhrase = [Microsoft.SqlServer.Management.SqlParser.Parser.Parser]::Parse($phrase, $parseOptions) + $parsedPhraseTokens = @($parsedPhrase.Script.Tokens.Type) + $parsedPhraseTokensAsString = $parsedPhraseTokens -join ' ' + $parsedPhraseCount = ([regex]::Matches($parsedTokensAsString, $parsedPhraseTokensAsString )).Count + + if ($parsedPhraseCount -gt 0) { + $errors += $phrase + } + } + + if (-not (Test-IsCollectionNullOrEmpty -Collection $errors)) { + Write-Error "$logLead : Your script violated the constraints on containing the following restricted keywords: `n `n$($errors -join "`n")`n `nYour script can not be run." + return $false + } + + # use the power of regex to count the number of occurrences + $beginTransactionCount = ([regex]::Matches($parsedTokensAsString, "TOKEN_BEGIN LEX_WHITE TOKEN_TRANSACTION" )).Count + $commitTransactionCount = ([regex]::Matches($parsedTokensAsString, "TOKEN_COMMIT LEX_WHITE TOKEN_TRANSACTION" )).Count + $rollbackTransactionCount = ([regex]::Matches($parsedTokensAsString, "TOKEN_ROLLBACK LEX_WHITE TOKEN_TRANSACTION" )).Count + + # You should have as many or more end-transaction statements as begin statements. This allows for an IF .. ROLLBACK .. ELSE .. COMMIT type scenario + $endTransactionCount = $commitTransactionCount + $rollbackTransactionCount + if ($beginTransactionCount -lt $endTransactionCount) { + Write-Error "$logLead : Your statement has a mismatched number of BEGIN TRANSACTION and either COMMIT TRANSACTION or ROLLBACK TRANSACTION statements. Please resolve this issue." + return $false + } + + return $true +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Confirm-DatabaseAccess.ps1 b/Modules/Alkami.PowerShell.Database/Public/Confirm-DatabaseAccess.ps1 new file mode 100644 index 0000000..a0d7368 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Confirm-DatabaseAccess.ps1 @@ -0,0 +1,75 @@ +function Confirm-DatabaseAccess { + <# + + .SYNOPSIS + This function confirms that the user running this script has access to the requested connection string. + + .DESCRIPTION + This function confirms that the user running this script has access to the requested connection string. + It uses the credentials of the connection string to connect. + + .PARAMETER ConnectionString + Used to test connection to the server. If not supplied, the AlkamiMaster from the machine.config is used, if available + + .PARAMETER ConnectionTimeout + SQL Connection Timeout in Seconds. Defaults to 15. + + .INPUTS + Connection string to connect to the server + + .OUTPUTS + Boolean state based on whether or not the connection attempt was successful. + + .EXAMPLE + Confirm-DatabaseAccess + + Confirm-DatabaseAccess + #> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$ConnectionString, + + [Parameter(Mandatory = $false, Position = 1)] + [int]$ConnectionTimeout = 15 + ) + process { + $logLead = (Get-LogLeadName) + + try { + if ([string]::IsNullOrEmpty($connectionString)) { + ## If there is no passed in connection string, use the current computer connection string + Write-Verbose "$logLead : Attempting to Retrieve Connection String from the machine.config" + $connectionString = (Get-ConnectionString 'AlkamiMaster') + } + + Write-Verbose "$logLead : Creating a ConnectionStringBuilder Object from the connection string" + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder "$($connectionString.ToString())" + $safeConnectionString = New-Object System.Data.SqlClient.SqlConnectionStringBuilder "$($connectionString.ToString())" + + $conStrBuilder['Connection Timeout'] = $ConnectionTimeout + $safeConnectionString['Connection Timeout'] = $ConnectionTimeout + + if (-not (Test-StringIsNullOrEmpty -Value $safeConnectionString.Password)) { + $safeConnectionString.Password = "Redacted" + } + + Write-Verbose "$logLead : Testing Connection Using ConnectionString: $($safeConnectionString.ToString())" + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $conStrBuilder.ToString() + $sqlConnection.Open() + $sqlConnection.Close() + + Write-Verbose "$logLead : Was able to connect to $connectionString" + + return $true + } catch { + Write-Warning "$logLead : Can not connect to the specified database. Do you have approved access to the server?" + return $false + } finally { + if ($null -ne $sqlConnection) { + $sqlConnection.Dispose() + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/ConvertTo-WhatIfQuery.ps1 b/Modules/Alkami.PowerShell.Database/Public/ConvertTo-WhatIfQuery.ps1 new file mode 100644 index 0000000..1b50ad5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/ConvertTo-WhatIfQuery.ps1 @@ -0,0 +1,11 @@ +function ConvertTo-WhatIfQuery { + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Query + ) + + return "BEGIN TRANSACTION; $Query; ROLLBACK TRANSACTION;" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-AllAWSEnvironmentDataTenants.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-AllAWSEnvironmentDataTenants.ps1 new file mode 100644 index 0000000..d7bd843 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-AllAWSEnvironmentDataTenants.ps1 @@ -0,0 +1,106 @@ +function Get-AllAWSEnvironmentDataTenants { +<# +.SYNOPSIS + Build a tenant list from the given folder of the aws-environment-data repository. If not provided, assumed to be the current folder. + +.DESCRIPTION + This function uses the aws-environment-data folder to parse for all the servers to go to to find the connection string. + It tries to spam this work out so we don't have to spend too much effort waiting on responses. + +.PARAMETER RepositoryPath + Where to find the aws-environment-data folder + +.PARAMETER FileFilter + Which files do we look for? This is used in the context of Get-ChildItem + +.EXAMPLE + Get-AllAWSEnvironmentDataTenants -RepositoryPath C:\git\aws-environment-data -FileFilter "Dev\*.txt" + +A long array of sites + +.EXAMPLE + Get-AllAWSEnvironmentDataTenants + +A long array of sites +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$RepositoryPath = ".", + + [Parameter(Mandatory = $false, Position = 1)] + [string]$FileFilter = "*.txt" + ) + + $logLead = Get-LogLeadName + + $RepositoryPath = Resolve-Path $RepositoryPath + + Write-Host "$logLead : Fetching all paths from $RepositoryPath (filter $FileFilter) for parsing" + + $fullPath = Join-Path $RepositoryPath $FileFilter + $files = (Get-ChildItem -Path $fullPath -Recurse -ErrorAction Ignore) + + $allEnvServers = Invoke-Parallel -Objects $files -ReturnObjects -Script { + param($file) + + # The next line wasn't working in my testing, so I went with the Path.GetFnameWoExt method below + # $envName = $file.BaseName + $envName = [System.IO.Path]::GetFilenameWithoutExtension($file.Name) + + $rawServers = (Get-Content $file.FullName) -split ',' + + $testableServers = @() + $testableServers += Select-AlkamiMicServers $rawServers + $testableServers += Select-AlkamiAppServers $rawServers + + if (!(Test-IsCollectionNullOrEmpty $testableServers.Count)) { + return @{ + EnvName = $envName + Servers = $testableServers + # We use this to know if we should go to NewRelic for data or the database + # Some files are called "preprod" but are _not_ prod with synthetics + # We should only match if the parent path matches Prod (or maybe DR if that filter is used?) + IsProd = $file.Directory -match 'Prod' + } + } + } + + $allConnections = Invoke-Parallel -Objects $allEnvServers -ReturnObjects -Script { + param($envServers) + $connectionString = "" + + # iterate each server because if the first one gives us the result, stop there + # No need to go to four servers if one works + foreach($server in $envServers.Servers) { + $connectionString = Invoke-Command -ComputerName $server -ScriptBlock { Get-MasterConnectionString } -ErrorAction Ignore + + if (![string]::IsNullOrWhitespace($connectionString)) { + break + } + } + + if (![string]::IsNullOrWhitespace($connectionString)) { + return @{ EnvName = $envServers.EnvName; ConnectionString = $connectionString; IsProd = $envServers.IsProd; } + } + } + + # Now that we have all of the connection strings, let's go get all the tenants from each environment: + $allTenants = Invoke-Parallel -Objects $allConnections -ReturnObjects -Script { + param($envConnectionString) + + $errorString = "" + try { + Write-Verbose "Connecting to $($envConnectionString.ConnectionString)" + $tenants = @(Get-FullTenantListFromServer $envConnectionString.ConnectionString) + } catch { + $errorString = "Could not connect to client: $($envConnectionString.EnvName) error: $($_.Exception.Message)" + } + + if (!(Test-IsCollectionNullOrEmpty $tenants) -or !([string]::IsNullOrWhitespace($errorString))) { + return @{ EnvName = $envConnectionString.EnvName; IsProd = $envConnectionString.IsProd; ConnectionString = $envConnectionString.ConnectionString; Tenants = $tenants; ErrorString = $errorString; } + } + } + + return $allTenants +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-DatabaseInstallPath.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-DatabaseInstallPath.ps1 new file mode 100644 index 0000000..3b4f9b4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-DatabaseInstallPath.ps1 @@ -0,0 +1,11 @@ +function Get-DatabaseInstallPath { +<# +.SYNOPSIS + Get the path to the default location for database installation +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + return "C:\Database" +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-DeveloperTenant.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-DeveloperTenant.ps1 new file mode 100644 index 0000000..51f6459 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-DeveloperTenant.ps1 @@ -0,0 +1,20 @@ +function Get-DeveloperTenant { +<# +.SYNOPSIS + Get the default developer tenant in a way that matches what is provided in the AlkamiMaster tenant table record +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param() + + return @{ + Name = 'Developer Dynamic'; + BankGuid = '78554577-9DE6-43CD-9085-5868977156D1'; + Signature = 'developer.dev.alkamitech.com'; + AdminSignature = 'admin-developer.dev.alkamitech.com'; + DataSource = 'localhost'; + Catalog = 'DeveloperDynamic'; + Version = ''; + ConnectionString = 'data source=localhost;Integrated Security=SSPI; Database=DeveloperDynamic;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' + }; +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-FormattedConnectionString.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-FormattedConnectionString.ps1 new file mode 100644 index 0000000..f46a16e --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-FormattedConnectionString.ps1 @@ -0,0 +1,72 @@ +function Get-FormattedConnectionString { +<# +.SYNOPSIS + Generate a formatted connection string for integrated security access + +.DESCRIPTION + Generate a formatted connection string for integrated security access + +.PARAMETER serverName + [string] Used to test connection to the server + +.PARAMETER instanceName + [string] Instance name to use for testing + +.PARAMETER databaseName + [string] Database name to check exists + +.INPUTS + Server name, instance name, database name + +.OUTPUTS + A connection string according to the expected inputs + +.EXAMPLE + Get-FormattedConnectionString -ServerName . -DatabaseName DeveloperDynamic + +Get-FormattedConnectionString -ServerName . -DatabaseName DeveloperDynamic + +"Data Source=.\;Integrated Security=SSPI; Database=DeveloperDynamic;" + +.EXAMPLE + Get-FormattedConnectionString . AlkamiMaster + +Get-FormattedConnectionString . AlkamiMaster + +"Data Source=.\;Integrated Security=SSPI; Database=AlkamiMaster;" +#> + [CmdletBinding()] + [OutputType([System.String])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Justification="Array Consolidation is Acceptable")] + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$serverName, + + [Parameter(Mandatory=$true, Position=1)] + [string]$databaseName, + + [Parameter(Mandatory=$false, Position=2)] + [string]$instanceName + ) + process { + if ([System.String]::IsNullOrEmpty($serverName) -and [System.String]::IsNullOrEmpty($instanceName)) { + Write-Verbose "No server or instance names passed in, returning default connection string for server" + return (Get-ConnectionString 'AlkamiMaster') + } + + $serverName = ($serverName, "." -ne $null)[0] + + $instanceName = ($instanceName, "" -ne $null)[0] + + if ($serverName.EndsWith("\\")){ + $serverName = $serverName.Substring(0, $serverName.Length - 1) + } + + $connectionString = "Data Source=$serverName\$instanceName;Integrated Security=SSPI; Database=$databaseName;" + ## TODO - bmorris - add multi cluster failover support + + Write-Verbose "Found $connectionString" + + return $connectionString + } +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-FullTenantListFromServer.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-FullTenantListFromServer.ps1 new file mode 100644 index 0000000..830387e --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-FullTenantListFromServer.ps1 @@ -0,0 +1,173 @@ +function Get-FullTenantListFromServer { + <# + +.SYNOPSIS + This function retrieves a full list of tenants from a given server identified by connection string + +.DESCRIPTION + This function retrieves a full list of tenants from a given server identified by connection string + +.PARAMETER connectionString + [string] Used to identify the connection to insert tenants to + +.INPUTS + Connection string to connect to the server. + +.OUTPUTS + A list of tenants. This is an array of hashmap objects. The objects should look like this example for developer tenant: + @{ + Name = 'Developer Dynamic'; + BankGuid = '78554577-9DE6-43CD-9085-5868977156D1'; + Signature = 'developer.dev.alkamitech.com'; + AdminSignature = 'admin-developer.dev.alkamitech.com'; + DataSource = 'localhost'; + Catalog = 'DeveloperDynamic'; + Version = ''; + ConnectionString = 'data source=localhost;Integrated Security=SSPI; Database=DeveloperDynamic;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;'; + GlobalIdentifier = '08bc7221-ba19-478b-889f-316c43324af2' + }; + +.EXAMPLE + Get-FullTenantListFromServer ConnectionString + + Get-FullTenantListFromServer 'data source=localhost;Integrated Security=SSPI; Database=AlkamiMaster;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' + + @(@{ + Name = 'Developer Dynamic'; + BankGuid = '78554577-9DE6-43CD-9085-5868977156D1'; + Signature = 'developer.dev.alkamitech.com'; + AdminSignature = 'admin-developer.dev.alkamitech.com'; + DataSource = 'localhost'; + Catalog = 'DeveloperDynamic'; + Version = ''; + ConnectionString = 'data source=localhost;Integrated Security=SSPI; Database=DeveloperDynamic;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;'; + GlobalIdentifier = '08bc7221-ba19-478b-889f-316c43324af2' + }) + +.NOTES + Will return GlobalIdentifier from Tenant table if column exist, else will return NULL for that column. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$connectionString + ) + process { + $logLead = Get-LogLeadName + + if ([string]::IsNullOrEmpty($connectionString)) { + ## If there is no passed in connection string, use the current computer connection string. + $connectionString = Get-MasterConnectionString + } + + if (!(Confirm-DatabaseAccess $connectionString)) { + throw "$logLead : could not connect to the database!" + } + + try { + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString + $sqlConnection.Open() + + $queryBankinstanceidentifierStatus = @" + IF COL_LENGTH('[Tenant]','bankinstanceidentifier') IS NOT NULL + SELECT '1' AS [Status]; + ELSE + SELECT '0' AS [Status]; +"@ + + $queryTenantFull = @" + SELECT + Name, + BankIdentifiers, + BankUrlSignatures, + BankAdminUrlSignatures, + DataSource, + Catalog, + Version, + ConnectionString, + BankInstanceIdentifier + FROM + tenant; +"@ + $queryTenantPartial = @" + SELECT + Name, + BankIdentifiers, + BankUrlSignatures, + BankAdminUrlSignatures, + DataSource, + Catalog, + Version, + ConnectionString, + NULL AS BankInstanceIdentifier + FROM + tenant; +"@ + + $data = @() + + try { + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + + # Run query to determine if column exists. + $command.CommandText = $queryBankinstanceidentifierStatus + + Write-Verbose "$logLead : Running query to determine if Tenant.BankInstanceIdentifier exists." + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + + # Find if the column exists. + while ($reader.Read()) { + $hasGLOBIDBit = $reader[0] + } + + $reader.Close() + + Write-Verbose "$logLead : GlobIdBit = $hasGLOBIDBit" + + if($hasGLOBIDBit -eq "1") { + Write-Verbose "$logLead : Tenant table has BankInstanceIdentifier column. Using queryTenantFull" + $command.CommandText = $queryTenantFull + } else { + Write-Verbose "$logLead : Tenant table does not have BankInstanceIdentifier column. Using queryTenantPartial" + $command.CommandText = $queryTenantPartial + } + + # Run the actual tenant query. + Write-Verbose "$logLead : Running tenant query." + $reader = $command.ExecuteReader() + + while ($reader.Read()) { + $data += @{ + Name = $reader[0]; + BankGuid = $reader[1]; + Signature = $reader[2]; + AdminSignature = $reader[3]; + DataSource = $reader[4]; + Catalog = $reader[5]; + Version = $reader[6]; + ConnectionString = $reader[7]; + Bankinstanceidentifier = $reader[8]; + }; + } + } catch { + Write-Host $_.Exception.Message + throw "$logLead : An error occurred getting tenant information from the database." + } finally { + Write-Verbose "$logLead : Closing and disposing DataReader and SqlConnection" + $reader.Dispose() + $sqlConnection.Close() + } + + foreach ($record in $data) { + if ([string]::IsNullOrWhiteSpace($record.ConnectionString)) { + $record.ConnectionString = Get-FormattedConnectionString -ServerName $record.DataSource -DatabaseName $record.Catalog + } + } + + return $data + } catch { + Write-Host $_.Exception.Message + throw "$logLead : An error occurred getting the tenant list from the server." + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerExe.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerExe.ps1 new file mode 100644 index 0000000..4466415 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerExe.ps1 @@ -0,0 +1,46 @@ +function Get-MigrationRunnerExe { + <# +.SYNOPSIS + Returns path to the appropriate MigrationRunner utility for the Runtime provided + +.PARAMETER Runtime + Runtime or framework of the package that the MigrationRunner will be running Migrations for + +.OUTPUTS + Returns the Path to the appropriate MigrationRunner utility +#> + [CmdletBinding()] + [OutputType([string])] + Param ( + [Parameter(Mandatory = $true)] + [ValidateSet("framework","dotnetcore")] + [string]$Runtime + ) + # SRE-16977 - This is overkill on Write-Hosts and safety valves. + # Leave them in until all the moving parts of the new Migration pipeline are in. + $loglead = Get-LogLeadName + Write-Host "$loglead : running on $($env:COMPUTERNAME) by $($env:USERNAME) started at $(Get-Date)" + if ((Test-StringIsNullOrEmpty (Get-EnvironmentVariable -StoreName Process -Name "sre_migrationUtility_tool")) -eq $true) { + $migrationUtilityParentPath = "C:\ProgramData\Alkami\SRE\MigrationUtility" + } else { + $migrationUtilityParentPath = Join-Path -Path (Get-EnvironmentVariable -StoreName Process -Name "sre_migrationUtility_tool") -ChildPath "dist" + } + $migrationRunnerExePath = "" + $NETFX_MIGRATIONRUNNER_PATH = Join-Path -Path $migrationUtilityParentPath -ChildPath "\Framework\Alkami.TeamCity.MigrationRunnerFramework.exe" + $DOTNETCORE_MIGRATIONRUNNER_PATH = Join-Path -Path $migrationUtilityParentPath -ChildPath "\Core\Alkami.TeamCity.MigrationRunnerCore.exe" + + switch ($Runtime) { + "framework" { $migrationRunnerExePath = $NETFX_MIGRATIONRUNNER_PATH } + "dotnetcore" { $migrationRunnerExePath = $DOTNETCORE_MIGRATIONRUNNER_PATH } + default { throw "$loglead : Could not find an appropriate MigrationRunner utility" } + } + + if (-not (Test-Path -Path $migrationRunnerExePath)) { + throw "$loglead : Runtime $Runtime Migration Runner path not found at $migrationRunnerPath. Please install Alkami.SRE.MigrationUtility in the target environment." + } + + Write-Host "$loglead : For Runtime $Runtime, the following MigrationRunner was found" + Write-Host "$loglead : $migrationRunnerExePath" + Write-Host "$loglead : finished at $(Get-Date)" + return $migrationRunnerExePath +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerPath.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerPath.ps1 new file mode 100644 index 0000000..fa3a5d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-MigrationRunnerPath.ps1 @@ -0,0 +1,15 @@ +function Get-MigrationRunnerPath { +<# +.SYNOPSIS + Get the path to the default location for fluent migrator libraries +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + $path = "C:\ProgramData\Alkami\Alkami\MachineSetup\DatabaseCore" + if (!(Test-Path $path)) { + (New-Item -ItemType Directory -Path $path -Force -ErrorAction Ignore) | Out-Null + } + return $path +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-ProvidersFromTenantDatabase.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-ProvidersFromTenantDatabase.ps1 new file mode 100644 index 0000000..1726c3d --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-ProvidersFromTenantDatabase.ps1 @@ -0,0 +1,81 @@ +function Get-ProvidersFromTenantDatabase { + +<# +.SYNOPSIS + Queries a tenant database for provider data + +.DESCRIPTION + Queries a tenant database for provider data. Optionally returns only valid provider configuration. Objects returned as a PSObject array contains + ProviderType, ProviderName, and AssemblyInfo + +.PARAMETER TenantConnectionString + The full connection string to a tenant database + +.PARAMETER ValidOnly + When supplied filters out providers tied to deleted Item records, or those with ProviderType Unknown + +.OUTPUTS + Returns objects hydrated from the JSON file. +#> + + [CmdletBinding()] + [OutputType([System.Object[]])] + param( + [Parameter(Mandatory=$true)] + [string]$TenantConnectionString, + + [Parameter(Mandatory=$false)] + [switch]$ValidOnly + ) + + $logLead = Get-LogLeadName + if (-NOT (Confirm-DatabaseAccess -connectionString $TenantConnectionString -InformationAction SilentlyContinue)) { + + Write-Warning "$logLead : Could not connect to Tenant with connection string [$TenantConnectionString]. Verify your access and rerun" + return $null + } + + $providerQuery = @" +SELECT pt.Name as ProviderType, p.Name as ProviderName, p.AssemblyInfo as ProviderAssemblyInfo +FROM core.provider p +INNER JOIN core.ProviderType pt on pt.ID = p.ProviderTypeID +INNER JOIN core.Item i on i.ParentId = p.ID +WHERE i.ItemType in ('Connector', 'Processor') +"@ + + if ($ValidOnly.IsPresent) { + + Write-Verbose "$logLead : Appending query with filter to eliminate invalid providers" + $providerQuery += "`n`tAND i.Deleted = '0' AND pt.Name != 'Unused'" + } + + $data = @() + try { + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $TenantConnectionString + $sqlConnection.Open() + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = $providerQuery + $reader = $command.ExecuteReader() + + while ($reader.Read()) { + + $data += @{ + ProviderType = $reader[0] + ProviderName = $reader[1] + AssemblyInfo = $reader[2] + } + } + } catch { + + Write-Warning "$logLead : An unexpected exception occurred: $($_.Exception.Message)" + return $null + } finally { + + if ($null -ne $sqlConnection) { + + $sqlConnection.Dispose() + } + } + + return $data +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Get-ReportServerCredentialsFromMaster.ps1 b/Modules/Alkami.PowerShell.Database/Public/Get-ReportServerCredentialsFromMaster.ps1 new file mode 100644 index 0000000..f4cdf7a --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Get-ReportServerCredentialsFromMaster.ps1 @@ -0,0 +1,90 @@ +function Get-ReportServerCredentialsFromMaster { + <# + +.SYNOPSIS + Gets SSRS connection info from Master Db +.DESCRIPTION + Gets SSRS connection info from Master Db edw.DataEngineSettings table + if creds are not setup, will return null + Access info with $returnedData['ReportServerPath'] + +.PARAMETER MasterConnectionString + [string] Used to test connection to the server +.EXAMPLE + Get-ReportServerCredentialsFromMaster -MasterConnectionString "data source=DC00DB01;Integrated Security=SSPI;Database=AlkamiMaster_ci1;MultiSubnetFailover=true" + +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string]$MasterConnectionString + ) + + $loglead = Get-LogLeadName + + if ($null -eq $MasterConnectionString) { + Write-Warning "[$loglead] : Connection string not supplied, Falling back to web" + + } + + Write-host "Testing Connection to $MasterConnectionString" + if ((Confirm-DatabaseAccess -connectionString $MasterConnectionString) -eq $false) { + Write-Error "[$loglead] : Can't Connect to $MasterConnectionString" + return + } + + try { + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $MasterConnectionString + $sqlConnection.Open() + + $query = @" + select 1 from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = 'edw' and TABLE_NAME = 'DataEngineSettings' and COLUMN_NAME = 'IsEncrypted' +"@ + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = $query + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + + $isEncrypted = $null + while ($reader.Read()) { + $isEncrypted = $reader[0] + } + $reader.Dispose() + + if ($isEncrypted -ne 1) { + Write-host "SSRS Data not found, returning null" + return + } + + $query = @" + select + [Key], + case when IsEncrypted = 1 then[edw].[ufn_DecryptDataString](convert(varbinary(1000), [Value], 2)) else[Value] end as [Value] + from + [edw].[DataEngineSettings] +"@ + + $command.CommandText = $query + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + + $returnData = @{} + while ($reader.Read()) { + $returnData[$reader[0]] = $reader[1] + } + return $returnData + } + catch { + Write-Host $_.Exception.Message + Write-Error "$logLead : An error occurred ." + throw $_ + } + finally { + $reader.Dispose() + $sqlConnection.Close() + } +} + + + + diff --git a/Modules/Alkami.PowerShell.Database/Public/Import-DeveloperDynamicTenant.ps1 b/Modules/Alkami.PowerShell.Database/Public/Import-DeveloperDynamicTenant.ps1 new file mode 100644 index 0000000..fb7f0da --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Import-DeveloperDynamicTenant.ps1 @@ -0,0 +1,38 @@ +function Import-DeveloperDynamicTenant { +<# + +.SYNOPSIS + This function ensures the developer dynamic tenant exists on the provided connection string + +.DESCRIPTION + This function ensures the developer dynamic tenant exists on the provided connection string + +.PARAMETER connectionString + [string] Used to identify the connection to insert tenants to + +.INPUTS + Connection string to connect to the server. + +.OUTPUTS + Nothing + +.EXAMPLE + Import-DeveloperDynamicTenant ConnectionString + +Import-DeveloperDynamicTenant 'data source=localhost;Integrated Security=SSPI; Database=AlkamiMaster;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$connectionString + ) + process { + if (!(Test-IsDeveloperMachine)) { + throw 'this should only be run on a developer machine' + } + + $tenants = @( (Get-DeveloperTenant) ); + + Import-TenantsToServer $connectionString $tenants; + } +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Import-TenantsToServer.ps1 b/Modules/Alkami.PowerShell.Database/Public/Import-TenantsToServer.ps1 new file mode 100644 index 0000000..06e4a1e --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Import-TenantsToServer.ps1 @@ -0,0 +1,98 @@ +function Import-TenantsToServer { + <# + +.SYNOPSIS + This function imports a given list of tenants to a given server identified by connection string + +.DESCRIPTION + This function imports a given list of tenants to a given server identified by connection string + +.PARAMETER connectionString + [string] Used to identify the connection to insert tenants to + +.PARAMETER tenants + [array][hash] A list of tenants to insert. This is an array of hashmap objects. The objects should look like this example for developer tenant: + @{ + Name = 'Developer Dynamic'; + BankGuid = '78554577-9DE6-43CD-9085-5868977156D1'; + Signature = 'developer.dev.alkamitech.com'; + AdminSignature = 'admin-developer.dev.alkamitech.com'; + DataSource = 'localhost'; + Catalog = 'DeveloperDynamic'; + Version = ''; + ConnectionString = 'data source=localhost;Integrated Security=SSPI; Database=DeveloperDynamic;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' + }; + +.INPUTS + Connection string to connect to the server. List of tenants. + +.OUTPUTS + Nothing + +.EXAMPLE + Import-TenantsToServer ConnectionString TenantsArray + +$tenants = @(Get-DeveloperTenant) +Import-TenantsToServer 'data source=localhost;Integrated Security=SSPI; Database=AlkamiMaster;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' $tenants +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$connectionString, + + [Parameter(Mandatory = $true, Position = 1)] + $tenants + ) + process { + $tenants = @(($tenants, $null -ne @())[0]) + if ($tenants.length -eq 0) { + Write-Host "No tenants were provided. Can't update the database server $connectionString with the new records." + } + + if (!(Confirm-DatabaseAccess $connectionString)) { + throw "$logLead : could not connect to the database!" + } + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString + + try { + $sqlConnection.Open() + + foreach ($tenant in $tenants) { + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + + $command.CommandText = @" + IF NOT EXISTS (SELECT * FROM Tenant WHERE BankIdentifiers = @BankGuid) BEGIN + INSERT INTO Tenant (Name,BankIdentifiers,BankUrlSignatures,CreateDate,BankAdminUrlSignatures,DataSource,Catalog,Version,ConnectionString,BankInstanceIdentifier) + VALUES (@Name,@BankGuid,@Signature,GETDATE(),@AdminSignature,@DataSource,@Catalog,@Version,@ConnectionString,@BankInstanceIdentifier); + END +"@ + + ($command.Parameters.AddWithValue("@Name", $tenant.Name)) | Out-Null + ($command.Parameters.AddWithValue("@BankGuid", $tenant.BankGuid)) | Out-Null + ($command.Parameters.AddWithValue("@Signature", $tenant.Signature)) | Out-Null + ($command.Parameters.AddWithValue("@AdminSignature", $tenant.AdminSignature)) | Out-Null + ($command.Parameters.AddWithValue("@DataSource", $tenant.DataSource)) | Out-Null + ($command.Parameters.AddWithValue("@Catalog", $tenant.Catalog)) | Out-Null + ($command.Parameters.AddWithValue("@Version", $tenant.Version)) | Out-Null + ($command.Parameters.AddWithValue("@ConnectionString", $tenant.ConnectionString)) | Out-Null + ($command.Parameters.AddWithValue("@BankInstanceIdentifier", $tenant.BankGuid)) | Out-Null + + ($command.ExecuteNonQuery()) | Out-Null + } + + $sqlConnection.Close() + } catch { + if ($null -ne $sqlConnection) { + try { + $sqlConnection.Close() + } catch { + Write-Error "SQL Connection could not be closed." + } + } + + Write-Error "Error occured in database query execution." + throw $_.Exception + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Initialize-AlkamiDatabase.ps1 b/Modules/Alkami.PowerShell.Database/Public/Initialize-AlkamiDatabase.ps1 new file mode 100644 index 0000000..4e88be3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Initialize-AlkamiDatabase.ps1 @@ -0,0 +1,91 @@ +function Initialize-AlkamiDatabase { +<# +.SYNOPSIS + Initialize an Alkami Database +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + $connectionString, + $databaseName + ) + process{ + if (!(Test-IsDeveloperMachine)) { + throw 'this should only be run on a developer machine' + } + + if (($connectionString).ToString() -notmatch 'data source=localhost') { + Write-Warning "Not connecting to a local database instance. Not attempting to create a database for '$databaseName'" + return $false + } + + if (Test-DatabaseExists $connectionString $databaseName) { + return $true + } + + $dbPath = (Get-DatabaseInstallPath); + if (!(Test-Path $dbPath)){ + (New-Item -ItemType Directory -Force -Path $dbPath) | Out-Null + } + + if ($connectionString -match 'AlkamiMaster') { + $connectionString = $connectionString -replace 'AlkamiMaster','master' + } + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + + $command.CommandText = "CREATE DATABASE [$databaseName] CONTAINMENT = NONE" + + $sqlConnection.Open() + + if ($databaseName -eq "DeveloperDynamic") { + [System.Data.SqlClient.SqlCommand]$filePathsCommand = $sqlConnection.CreateCommand() + $filePathsCommand.CommandText = "SELECT SERVERPROPERTY('InstanceDefaultDataPath') as InstanceDefaultDataPath,SERVERPROPERTY('InstanceDefaultLogPath') as InstanceDefaultLogPath;" + + [System.Data.SqlClient.SqlDataReader]$reader = $filePathsCommand.ExecuteReader() + + $dataPath = "" + $logPath = "" + + if ($reader.Read()) { + $dataPath = (Join-Path $reader[0].ToString() "$databaseName.mdf") + $logPath = (Join-Path $reader[1].ToString() "$($databaseName)_log.ldf") + } else { + Write-Warning "No results from ExecuteReader" + } + $reader.Dispose() + + $backupFilepath = (Join-Path (Get-MigrationRunnerPath) 'DeveloperDynamic.bak') + if (Test-Path $backupFilepath) { + $command.CommandText = "RESTORE DATABASE [DeveloperDynamic] FROM DISK = N'$backupFilepath' WITH FILE = 1, MOVE N'DeveloperDynamic' TO N'$dataPath', MOVE N'DeveloperDynamic_log' TO N'$logPath', NOUNLOAD, REPLACE, STATS = 5" + } + } + + $command.ExecuteNonQuery() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "ALTER DATABASE [$databaseName] MODIFY FILE (NAME = N'$databaseName', MAXSIZE = UNLIMITED, FILEGROWTH = 10%);" + $command.ExecuteNonQuery() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "ALTER DATABASE [$databaseName] MODIFY FILE (NAME = N'$($databaseName)_log', MAXSIZE = UNLIMITED, FILEGROWTH = 10%);" + $command.ExecuteNonQuery() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "ALTER DATABASE [$databaseName] COLLATE SQL_Latin1_General_CP1_CI_AS;" + $command.ExecuteNonQuery() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "ALTER DATABASE [$databaseName] SET COMPATIBILITY_LEVEL = 110;" + $command.ExecuteNonQuery() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "EXEC sp_change_users_login 'Auto_Fix', 'user';" + $command.ExecuteNonQuery() + + $sqlConnection.Close() + + return $true + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiLegacyOrbMigrations.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiLegacyOrbMigrations.ps1 new file mode 100644 index 0000000..4c6fbb7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiLegacyOrbMigrations.ps1 @@ -0,0 +1,121 @@ +function Invoke-AlkamiLegacyOrbMigrations { +<# +.SYNOPSIS + Run migrations against ORB tenant, master, etc databases by calling Invoke-AlkamiMigrationRunner + +.DESCRIPTION + Used to call the Alkami MigrationUtility for running migrations against various systems + +.PARAMETER ConnectionString + The connection string of the AlkamiMaster database to run migrations against. + +.PARAMETER OrbMigrateFolderPath + The location where ORB migration dlls are located. This is typically supplied as part of the deployment process. + +.PARAMETER Parallelism + This value determines how many threads/workers are used in parallel to the service. + Values should be supplied higher than 0. A value of -1 indicates to run as many threads as possible. + +.PARAMETER LogFileFolder + When supplied, the output of the migration runner will be redirected to the specified path. + If the folder does not exist, it will be created. + If the file already exists, it will be overwritten. + If the PackageId and PackageVersion are provided, the results will be directed to a file with the name format of "{package id}.{version}.log" in the specified folder. + Otherwise the log file will be written to a file named after the MigrationTypeName value with the Process.ID appended like so "{migration type name}.{process id}.log" + NOTE: This parameter should not be used in a TeamCity process as it will not emit anything to the TeamCity process. + +.PARAMETER WhatIf + Supply this parameter to see what would happen if the utility were to run to completion. + +.PARAMETER Tags + Supply this parameter to FluentMigrations for ex: SDK + +.PARAMETER LogTeamCityMessages + Required to log team city control messages at all + Omitting the LogTeamCityMessages flag will not cause messages to be ommitted, for that you should set the global logging to NONE, but will instead omit the use of TeamCity Service Messages, such as ##teamcity(message + +.PARAMETER NoTeamCityBlockMessages + Will prevent OpenBlock and CloseBlock messages from happening + +.PARAMETER NoTeamCityBuildProblems + Will prevent BuildProblems from emitting as a build problem + +.PARAMETER NoTeamCityMessages + Will prevent Messages from being written (you should probably just not /LogTeamCityMessages if you get to this point) + +.PARAMETER LogErrorsAsWarn + Will not use the ERROR message format, all will goto WARN, this is to prevent certain TC job steps from stopping the pipeline + +.OUTPUTS + This cmdlet does not return anything directly, but there will be a $EXITCODE value set, + and this process will emit TeamCity BuildError and Message when run on TeamCity servers. + + On any other environment, logs will be emitted to the console (standard output) and may be captured + +.LINK + Get-MigrationRunnerExe + +#> + [CmdletBinding()] + [OutputType([void])] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Use of WhatIf flag intentional so we track the presence of it")] + param ( + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ConnectionString, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$OrbMigrateFolderPath, + + # Has a default value in the app already, no need to configure it unless desired + [Parameter(Mandatory = $false)] + [int]$Parallelism, + + [Parameter(Mandatory = $false)] + [string]$LogFileFolder, + + [Parameter(Mandatory = $false)] + [switch]$WhatIf, + + [Parameter(Mandatory = $false)] + [string[]]$Tags, + + [Parameter(Mandatory = $false)] + [Alias('EnableTeamCityServiceMessages')] + [switch]$LogTeamCityMessages, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityBlocks")] + [switch]$NoTeamCityBlockMessages, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityBuildProblems")] + [switch]$NoTeamCityBuildProblems, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityMessages")] + [switch]$NoTeamCityMessages, + + [Parameter(Mandatory = $false)] + [Alias("LogTeamCityErrorMessagesAsWarn")] + [switch]$LogErrorsAsWarn + ) + + $migrationSplat = @{ + ConnectionString = $ConnectionString + MigrationTypeName = "orb" + OrbMigrateFolderPath = $OrbMigrateFolderPath + WhatIf = $WhatIf + LogFileFolder = $LogFileFolder + Parallelism = $Parallelism + LogTeamCityMessages = $LogTeamCityMessages + NoTeamCityBlockMessages = $NoTeamCityBlockMessages + NoTeamCityBuildProblems = $NoTeamCityBuildProblems + NoTeamCityMessages = $NoTeamCityMessages + LogErrorsAsWarn = $LogErrorsAsWarn + } + + Invoke-AlkamiMigrationRunner @migrationSplat +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.ps1 new file mode 100644 index 0000000..df75904 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.ps1 @@ -0,0 +1,570 @@ +function Invoke-AlkamiMigrationRunner { + <# +.SYNOPSIS + Run migrations against ORB tenant, master, etc databases + +.DESCRIPTION + Used to call the Alkami MigrationUtility for running migrations against various systems + ParameterSets + ORB = TeamCity ORB (Update hyphen AlkamiDatabase), SDKSetup, Developers, DevLeads RunMigrations for ORB + TeamCityInstallPackages = TeamCity Install_Packages, RunMigrationsOnServiceAgainstTarget + Runtime = Developers + +.PARAMETER MigrationRunnerPath + The path that executes the migrations. This is dependent on the framework the migrations were developed in. + The expectation is that they would match their hosting service if they are bundled together. + +.PARAMETER MigrationTypeName + The matched name to the type of migration being run. + This has to align with the valid type names in the SRE.MigrationUtility code -> Alkami.TeamCity.MigrationRunnerShared.Configuration namespace. + +.PARAMETER Runtime + As an alternative to the MigrationTypeName and MigrationRunnerPath, you can specify the package runtime. + By specifying this parameter, you are not required to provide the feed, dbmsusers, subscriptionhost, and other parameters. + +.PARAMETER PackageId + The ID of the package being installed. + +.PARAMETER PackageVersion + The version of the package being installed. + +.PARAMETER Feed + The feed where the package can be found. + +.PARAMETER ConnectionString + The connection string to run migrations against. + On a developer machine, this can be retrieved from the machine.config automatically when using the -Runtime parameter. + +.PARAMETER DbmsUser + The users the migrations are to be run targeting for typical use-cases + On a developer machine, this can be retrieved from the machine.config automatically when using the -Runtime parameter. + +.PARAMETER DatabaseServiceAccount + This value should normally be supplied via manifest, but may be specified manually if invoked manually. + +.PARAMETER SubscriptionHost + This is the hostname or IP address of the service that runs the subscription service. + This value is particularly valuable for migrations that talk to services during the runtime. + Example: Notification template registration that occurs once per tenant database that calls a service to register the notification templates. + +.PARAMETER TempFolder + The location where files should be downloaded to. + +.PARAMETER EnvironmentType + The environment type that the migrations are targeting. + +.PARAMETER OrbMigrateFolderPath + The location where ORB migration dlls are located. This is typically supplied as part of the deployment process. + +.PARAMETER Parallelism + This value determines how many threads/workers are used in parallel to the service. + Values should be supplied higher than 0. A value of -1 indicates to run as many threads as possible. + +.PARAMETER SqlTimeout + This value determines how long a sql command should take in seconds. + Values should be supplied higher than 0. A value of -1 indicates no timeout. + Default timeout is 10 minutes + +.PARAMETER LogFileFolder + When supplied, the output of the migration runner will be redirected to the specified path. + If the folder does not exist, it will be created. + If the file already exists, it will be overwritten. + If the PackageId and PackageVersion are provided, the results will be directed to a file with the name format of "{package id}.{version}.log" in the specified folder. + Otherwise the log file will be written to a file named after the MigrationTypeName value with the Process.ID appended like so "{migration type name}.{process id}.log" + NOTE: This parameter should not be used in a TeamCity process as it will not emit anything to the TeamCity process. + +.PARAMETER WhatIf + Supply this parameter to see what would happen if the utility were to run to completion. + +.PARAMETER Tags + Supply this parameter to FluentMigrations for ex: SDK + +.PARAMETER LegacyFluentMigratorContext + Provided for legacy SDK migration compatability + +.PARAMETER LogTeamCityMessages + Required to log team city control messages at all + Omitting the LogTeamCityMessages flag will not cause messages to be ommitted, + (for that you should set the global logging to NONE) + but will instead omit the use of TeamCity Service Messages, such as ##teamcity(message... + +.PARAMETER NoTeamCityBlockMessages + Will prevent OpenBlock and CloseBlock messages from happening + +.PARAMETER NoTeamCityBuildProblems + Will prevent BuildProblems from emitting as a build problem + +.PARAMETER NoTeamCityMessages + Will prevent Messages from being written (you should probably just not /LogTeamCityMessages if you get to this point) + +.PARAMETER LogErrorsAsWarn + Will not use the ERROR message format, all will goto WARN, this is to prevent certain TC job steps from stopping the pipeline + +.OUTPUTS + This cmdlet does not return anything directly, but there will be a $EXITCODE value set, + and this process will emit TeamCity BuildError and Message when run on TeamCity servers. + + On any other environment, logs will be emitted to the console (standard output) and may be captured + +.LINK + Get-MigrationRunnerExe + +#> + [CmdletBinding(DefaultParameterSetName = 'TeamCityInstallPackages')] + [OutputType([void])] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Use of WhatIf flag intentional so we track the presence of it")] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'GrantRoleTenants')] + [Parameter(Mandatory = $false, ParameterSetName = 'DbUserCreate')] + [Parameter(Mandatory = $false, ParameterSetName = 'GrantRole')] + [Parameter(Mandatory = $false, ParameterSetName = 'ORB')] + [ValidateNotNullOrEmpty()] + [string]$MigrationRunnerPath, + + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRoleTenants')] + [Parameter(Mandatory = $true, ParameterSetName = 'DbUserCreate')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRole')] + [Parameter(Mandatory = $true, ParameterSetName = 'ORB')] + [ValidateSet('choco', 'orb', 'GrantRoleTenants', 'DbUserCreate','GrantRole')] + [string]$MigrationTypeName, + + [Parameter(Mandatory = $true, ParameterSetName = 'DeveloperEnvironment')] + [ValidateNotNullOrEmpty()] + [string]$Runtime, + + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'DeveloperEnvironment')] + [ValidateNotNullOrEmpty()] + [string]$PackageId, + + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'DeveloperEnvironment')] + [ValidateNotNullOrEmpty()] + [string]$PackageVersion, + + # Required = false because on a developer machine this can be found locally sometimes as an installed package. + # Required = false because we default to the choco dev feed. + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [Alias("Feed")] + [string]$PackageFeed, + + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [object]$Package, + + [Parameter(Mandatory = $true, ParameterSetName = 'ManuallyApplyServiceAccount')] + [Parameter(Mandatory = $true, ParameterSetName = 'ORB')] + # Required = false because on a developer machine this can be found locally + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRoleTenants')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRole')] + [string]$ConnectionString, + + # Required = false because on a developer machine this can be found locally + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [Parameter(Mandatory = $true, ParameterSetName = 'DbUserCreate')] + [string[]]$DbmsUser, + + # Required = false because on a developer machine this can be found locally + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [string]$SubscriptionHost, + + # Required = false because we default to the system temp folder. + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [string]$TempFolder, + + # Required = false because on a developer machine this can be found locally + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $true, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [string]$EnvironmentType, + + [Parameter(Mandatory = $false, ParameterSetName = 'TeamCityInstallPackages')] + [Parameter(Mandatory = $false, ParameterSetName = 'TeamCityInstallPackagesByPackage')] + [Parameter(Mandatory = $false, ParameterSetName = 'DeveloperEnvironment')] + [Parameter(Mandatory = $false, ParameterSetName = 'ORB')] + [Parameter(Mandatory = $false, ParameterSetName = 'GrantRoleTenants')] + [Parameter(Mandatory = $false, ParameterSetName = 'GrantRole')] + [string[]]$BankGuids, + + [Parameter(Mandatory = $true, ParameterSetName = 'ORB')] + [ValidateNotNullOrEmpty()] + [string]$OrbMigrateFolderPath, + + [Parameter(Mandatory = $true, ParameterSetName = 'ManuallyApplyServiceAccount')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRoleTenants')] + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRole')] + [string[]]$DatabaseServiceAccount, + + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRole')] + [string[]]$RoleName, + + [Parameter(Mandatory = $true, ParameterSetName = 'GrantRoleTenants')] + [string[]]$TenantRoleName, + + [Parameter(Mandatory = $true, ParameterSetName = 'DbUserCreate')] + [string]$DataSource, + + [Parameter(Mandatory = $true, ParameterSetName = 'DbUserCreate')] + [string]$DatabaseName, + + # Has a default value in the app already, no need to configure it unless desired + [Parameter(Mandatory = $false)] + [int]$Parallelism, + + # Has a default value in the app already, no need to configure it unless desired + [Parameter(Mandatory = $false)] + [int]$SqlTimeout, + + [Parameter(Mandatory = $false)] + [string]$LogFileFolder, + + [Parameter(Mandatory = $false)] + [switch]$WhatIf, + + [Parameter(Mandatory = $false)] + [string[]]$Tags, + + [Parameter(Mandatory = $false)] + [string[]]$LegacyFluentMigratorContext, + + [Parameter(Mandatory = $false)] + [Alias('EnableTeamCityServiceMessages')] + [switch]$LogTeamCityMessages, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityBlocks")] + [switch]$NoTeamCityBlockMessages, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityBuildProblems")] + [switch]$NoTeamCityBuildProblems, + + [Parameter(Mandatory = $false)] + [Alias("PreventTeamCityMessages")] + [switch]$NoTeamCityMessages, + + [Parameter(Mandatory = $false)] + [Alias("LogTeamCityErrorMessagesAsWarn")] + [switch]$LogErrorsAsWarn + ) + + $logLead = Get-LogLeadName + Write-Host "$loglead : running Invoke-AlkamiMigrationRunner on $($env:COMPUTERNAME) for $($env:USERNAME) at $(Get-Date)" + + $overriddenParameterSetName = $PSCmdlet.ParameterSetName + + if ($overriddenParameterSetName -eq 'TeamCityInstallPackagesByPackage') { + $PackageId = $Package.Name + $PackageVersion = $Package.Version + $PackageFeed = $Package.Feed.Source + + # Same path aside from the above variables + $overriddenParameterSetName = 'TeamCityInstallPackages' + } + + $writeToLog = $false + $logFilePath = "" + if (-not [string]::IsNullOrWhiteSpace($LogFileFolder)) { + if (-not (Test-Path -Path $LogFileFolder)) { + New-Item -ItemType Directory -Path $LogFileFolder -Force -WhatIf:$WhatIf | Out-Null + } + + # $PID is a magic variable + $logFilename = "$MigrationTypeName.$PID.log" + + if ((-not [string]::IsNullOrWhiteSpace($PackageId)) -and (-not [string]::IsNullOrWhiteSpace($PackageVersion))) { + $logFilename = "$PackageId.$PackageVersion.log" + } + + $logFilePath = Join-Path -Path $LogFileFolder -ChildPath $logFilename + Write-Host "$logLead : Writing the output of this function to [$logFilePath]" + $writeToLog = $true + } + + $finalMigrationRunnerPath = $MigrationRunnerPath + $finalMigrationTypeName = $MigrationTypeName + + # Validate or set parameter information + switch ($overriddenParameterSetName) { + 'TeamCityInstallPackages' { + Write-Host "$logLead : Running in packages mode" + $allRequiredParametersSupplied = $true + $requiredParameters = @('ConnectionString', 'DbmsUser', 'EnvironmentType', 'PackageFeed', 'SubscriptionHost', 'TempFolder') + foreach ($requiredParameter in $requiredParameters) { + if ([string]::IsNullOrWhiteSpace($PSCmdlet.MyInvocation.BoundParameters[$requiredParameter])) { + $allRequiredParametersSupplied = $false + Write-Warning "$logLead : Required parameter $requiredParameter was not provided. Please provide this value." + } + } + if (-not $allRequiredParametersSupplied) { + throw "$logLead : Please provide all required parameters." + } + } + 'ORB' { + Write-Host "$loglead : Running in ORB mode" + $finalMigrationTypeName = 'ORB' + if ([string]::IsNullOrWhiteSpace($MigrationRunnerPath)) { + $finalMigrationRunnerPath = Get-MigrationRunnerExe -Runtime 'framework' + } else { + if (-not (Test-Path -Path $MigrationRunnerPath)) { + throw "$logLead : MigrationRunnerPath invalid or not present. Please ensure the runner exists" + } + $finalMigrationRunnerPath = $MigrationRunnerPath + } + + if ([string]::IsNullOrWhiteSpace($ConnectionString)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + if (-not (Test-Path -Path $OrbMigrateFolderPath)) { + throw "$logLead : OrbMigrateFolderPath invalid or not present. Please ensure the folder exists" + } + } + 'DeveloperEnvironment' { + Write-Host "$loglead : Running in Developer Environment" + $finalMigrationTypeName = 'choco' + $validatedRuntime = Get-ValidatedRuntimeParameter -Runtime $Runtime + $finalMigrationRunnerPath = Get-MigrationRunnerExe -Runtime $validatedRuntime + } + 'ManuallyApplyServiceAccount' { + $finalMigrationTypeName = 'ManuallyApplyServiceAccount' + } + 'GrantRoleTenants' { + Write-Host "$loglead : Running in GrantRoleTenants mode" + $finalMigrationTypeName = 'GrantRoleTenants' + if ([string]::IsNullOrWhiteSpace($MigrationRunnerPath)) { + $finalMigrationRunnerPath = Get-MigrationRunnerExe -Runtime 'dotnetcore' + } else { + if (-not (Test-Path -Path $MigrationRunnerPath)) { + throw "$logLead : MigrationRunnerPath invalid or not present. Please ensure the runner exists" + } + $finalMigrationRunnerPath = $MigrationRunnerPath + } + if ([string]::IsNullOrWhiteSpace($ConnectionString)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + } + 'GrantRole' { + Write-Host "$loglead : Running in GrantRole mode" + $finalMigrationTypeName = 'GrantRole' + if ([string]::IsNullOrWhiteSpace($MigrationRunnerPath)) { + $finalMigrationRunnerPath = Get-MigrationRunnerExe -Runtime 'dotnetcore' + } else { + if (-not (Test-Path -Path $MigrationRunnerPath)) { + throw "$logLead : MigrationRunnerPath invalid or not present. Please ensure the runner exists" + } + $finalMigrationRunnerPath = $MigrationRunnerPath + } + if ([string]::IsNullOrWhiteSpace($ConnectionString)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + } + 'DbUserCreate' { + Write-Host "$loglead : Running in DbUserCreate mode" + $finalMigrationTypeName = 'DbUserCreate' + if ([string]::IsNullOrWhiteSpace($MigrationRunnerPath)) { + $finalMigrationRunnerPath = Get-MigrationRunnerExe -Runtime 'dotnetcore' + } else { + if (-not (Test-Path -Path $MigrationRunnerPath)) { + throw "$logLead : MigrationRunnerPath invalid or not present. Please ensure the runner exists" + } + $finalMigrationRunnerPath = $MigrationRunnerPath + } + if ([string]::IsNullOrWhiteSpace($DataSource)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + if ([string]::IsNullOrWhiteSpace($DatabaseName)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + if ([string]::IsNullOrWhiteSpace($DbmsUser)) { + throw "$logLead : Connection string must be supplied. Value provided was invalid." + } + } + } + + $argumentList = @() + $argumentList += "MigrationTypeName=$finalMigrationTypeName" + + # Convert potential array to a comma separated list of values + # When presented with a single string, does nothing. Yay auto-boxing. + # Depending on if you are using the pipeline and other things. Maybe, maybe not. + $finalDbmsUser = $DbmsUser -join ',' + $finalDatabaseServiceAccount = $DatabaseServiceAccount -join ',' + + switch ($finalMigrationTypeName) { + 'choco' { + $argumentList += "PackageId=`"$PackageId`"" + $argumentList += "PackageVersion=`"$PackageVersion`"" + + if (-not [string]::IsNullOrWhiteSpace($PackageFeed)) { + $argumentList += "Feed=`"$PackageFeed`"" + } + + if (-not [string]::IsNullOrWhiteSpace($ConnectionString)) { + $argumentList += "ConnectionString=`"$ConnectionString`"" + } + + if (-not [string]::IsNullOrWhiteSpace($finalDbmsUser)) { + $argumentList += "DbmsUser=`"$finalDbmsUser`"" + } + + if (-not [string]::IsNullOrWhiteSpace($SubscriptionHost)) { + $argumentList += "SubscriptionHost=`"$SubscriptionHost`"" + } + + if (-not [string]::IsNullOrWhiteSpace($EnvironmentType)) { + $argumentList += "EnvironmentType=`"$EnvironmentType`"" + } + + if (($Parallelism -gt 0) -or ($Parallelism -eq -1)) { + $argumentList += "Parallelism=$Parallelism" + } + + if (-not [string]::IsNullOrWhiteSpace($TempFolder)) { + $argumentList += "TempFolder=$TempFolder" + } + } + 'orb' { + $argumentList += "ConnectionString=$ConnectionString" + $argumentList += "OrbMigrateFolderPath=$OrbMigrateFolderPath" + } + 'ManuallyApplyServiceAccount' { + $argumentList += "ConnectionString=$ConnectionString" + $argumentList += "DatabaseServiceAccount=$finalDatabaseServiceAccount" + } + 'GrantRoleTenants' { + $argumentList += "ConnectionString=$ConnectionString" + $argumentList += "DatabaseUserName=$finalDatabaseServiceAccount" + $argumentList += "RoleName=$($TenantRoleName -join ",")" + } + 'GrantRole' { + $argumentList += "ConnectionString=$ConnectionString" + $argumentList += "DatabaseUserName=$finalDatabaseServiceAccount" + $argumentList += "RoleName=$($RoleName -join ",")" + } + 'DbUserCreate' { + $argumentList += "DataSource=$DataSource" + $argumentList += "DatabaseName=$DatabaseName" + $argumentList += "UserNames=$DbmsUser" + } + default { + # Can't get here since we use ValidateSet, but all the same, clarity helps + throw "Unrecognized migration type specified." + } + } + if (($SqlTimeout -gt 0) -or ($SqlTimeout -eq -1)) { + $argumentList += "SqlTimeout=$SqlTimeout" + } + if (-not [string]::IsNullOrWhiteSpace($BankGuids)) { + $argumentList += "BankGuids=`"$BankGuids`"" + } + + if (-not (Test-Path -Path $finalMigrationRunnerPath)) { + throw "$logLead : Could not find the migration runner path [$finalMigrationRunnerPath]" + } + + if ($null -ne $Tags) { + $argumentList += "Tags=$($Tags -join ',')" + } + + if ($null -ne $LegacyFluentMigratorContext) { + $argumentList += "LegacyFluentMigratorContext=$($LegacyFluentMigratorContext -join ',')" + } + + if ($WhatIf) { + $argumentList += "WhatIf=$WhatIf" + Write-Warning "$loglead : WHAT-IF MODE ENABLED!" + } + + if ($LogTeamCityMessages) { + $argumentList += "LogTeamCityMessages=true" + } + + if ($NoTeamCityBlockMessages) { + $argumentList += "NoTeamCityBlockMessages=true" + } + + if ($NoTeamCityBuildProblems) { + $argumentList += "NoTeamCityBuildProblems=true" + } + + if ($NoTeamCityMessages) { + $argumentList += "NoTeamCityMessages=true" + } + + if ($LogErrorsAsWarn) { + $argumentList += "LogErrorsAsWarn=true" + } + + $safeOutputString = ($argumentList -join "`n") -replace "password=.+;", "SECRET" + Write-Host "$loglead : Calling Invoke-CallOperatorWithPathAndParameters $finalMigrationRunnerPath with params:`n$safeOutputString" + if ($writeToLog) { + $results = Invoke-CallOperatorWithPathAndParameters $finalMigrationRunnerPath $argumentList + $results | ForEach-Object { if ($_.Contains("Error") -or $_.Contains("Fatal")) { Write-Host $_ } } + $results | Out-File -FilePath $logFilePath -Force -Encoding UTF8 + Write-Host "$logLead : Log file written to the following path`n`n`t$logFilePath`n`n" + } else { + Invoke-CallOperatorWithPathAndParameters $finalMigrationRunnerPath $argumentList + } + + if ($WhatIf -and -not $writeToLog) { + Write-Warning "$loglead : WHAT-IF MODE ENABLED!" + } + + <# + This is what we would add to Install_Packages to make reading things easier, and to make it easier to copy-paste by producing it now + $tcSplat = @{ + MigrationRunnerPath = $migratorExe + MigrationTypeName = 'choco' + PackageId = $package.Name + PackageVersion = $package.Version + Feed = $feed + ConnectionString = $masterDbConnectionString + DbmsUser = $DbmsUserString + SubscriptionHost = $subscriptionIP + EnvironmentType = $environmentType + TempFolder = $downloadDirectory + Parallelism = $parallelism + # Not accounted for in the new system (yet) + # Could cause issues for SDK developers tho ... + # Should not, because the package will already be installed on the system by the time this kicks off + # -FeedUserName $NugetCredential.UserName -FeedPassword (Get-PasswordFromCredential $NugetCredential) + } + + Invoke-AlkamiMigrationRunner @tcSplat + #> + + <# + This is what we add to TDC/Update-AlkamiDatabaseWithMigrationTool.ps1 + $tcSplat = @{ + MigrationRunnerPath = $migratorExe + MigrationTypeName = 'orb' + OrbMigrateFolderPath = $OrbMigrateFolderPath + ConnectionString = $masterDbConnectionString + Parallelism = $parallelism + } + + Invoke-AlkamiMigrationRunner @tcSplat + #> + + <# + # cole's debugging line from testing locally + --MigrationTypeName choco --packageId="Alkami.Migrations.UserReporting.Service.Host" packageVersion="1.2.4" feed="https://packagerepo.orb.alkamitech.com/nuget/choco.dev/" connectionString="Server=localhost;Database=AlkamiMaster;Integrated Security=true;" dbUser="corp\\dev.dbms$" SubscriptionHost=127.0.0.1 tempFolder="c:\temp" WhatIf=true EnvironmentType=Sandbox + # stuff from Install_Packages + & $migratorExe choco $package.Name $package.Version $feed $masterDbConnectionString $DbmsUserString $subscriptionIP $environmentType $downloadDirectory $parallelism -FeedUserName $NugetCredential.UserName -FeedPassword (Get-PasswordFromCredential $NugetCredential) + # TDC/Update-AlkamiDatabaseWithMigrationTool.ps1 + & $migratorExe "orb" "$masterDbConnectionString" -OrbMigrateFolderPath "$dbMigrationPath" + #> +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.tests.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.tests.ps1 new file mode 100644 index 0000000..6788ff3 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-AlkamiMigrationRunner.tests.ps1 @@ -0,0 +1,421 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" +$databaseModule = 'Alkami.PowerShell.Database' + +# define some common parameters so we can refer to them +$migrationRunnerPath = 'C:\This\Is\A\Path\runner.exe' +$migrationTypeName = 'choco' +$runtime = 'framework' +$packageId = 'testpackage' +$packageVersion = '0.0.1' +$feed = 'http://test.domain/feed/url' +$connectionString = 'Pretend this is a valid connection string' +$dbmsUser = 'domain\gmsa.account$' +$databaseServiceAccount = 'ServiceAccountPrefix' +$subscriptionhost = '127.0.0.1' +$orbMigrateFolderPath = 'C:\This\Is\A\Path\Migrations' +$tempFolder = 'C:\Windows\Temp' +$environmentType = 'Pester' +$logFileFolder = 'C:\This\Is\Not\A\Path' +$bankGuids = [Guid]::NewGuid().Guid,[Guid]::NewGuid().Guid + +# This is specifically a string-with-spaces so we can bypass "required" parameters so we can validate them elsewhere (if we validate them at all) +$missing = " " + +Describe "Invoke-AlkamiMigrationRunner_TeamCityInstallPackages" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + Context "Invalid parameters provided - Feed is provided" { + It "throws" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName $MigrationTypeName -PackageId $PackageId -PackageVersion $PackageVersion -PackageFeed $Feed -ConnectionString $missing -DbmsUser $missing -SubscriptionHost $missing -TempFolder $missing -EnvironmentType $missing } | Should -Throw + } + It "Called Write-Warning for each invalid parameter passed above with `$missing" { + # Feed is provided + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 5 -Scope Context + } + } + + Context "Invalid parameters provided - Feed is not provided" { + It "does not throw" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName $MigrationTypeName -PackageId $packageId -PackageVersion $packageVersion -PackageFeed $missing -ConnectionString $missing -DbmsUser $missing -SubscriptionHost $missing -TempFolder $missing -EnvironmentType $missing -BankGuids $missing } | Should -Throw + } + It "Called Write-Warning for each invalid parameter passed above with `$missing" { + # Feed is not provided + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 6 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName $MigrationTypeName -PackageId $PackageId -PackageVersion $PackageVersion -PackageFeed $Feed -ConnectionString $ConnectionString -DbmsUser $DbmsUser -SubscriptionHost $SubscriptionHost -TempFolder $TempFolder -EnvironmentType $EnvironmentType -BankGuids $BankGuids -LogTeamCityMessages -NoTeamCityBlockMessages -NoTeamCityBuildProblems -NoTeamCityMessages -LogErrorsAsWarn} | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 0 -Scope Context + } + It "Calls Get-ValidatedRuntimeParameter" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "Validish parameters provided - With logging" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName $MigrationTypeName -PackageId $PackageId -PackageVersion $PackageVersion -PackageFeed $Feed -ConnectionString $ConnectionString -DbmsUser $DbmsUser -SubscriptionHost $SubscriptionHost -TempFolder $TempFolder -EnvironmentType $EnvironmentType -BankGuids $BankGuids -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 0 -Scope Context + } + It "Calls Get-ValidatedRuntimeParameter" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Calls Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 1 -Scope Context + } + } +} + +Describe "Invoke-AlkamiMigrationRunner_Runtime" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { return 'ignored' } + # Tom, why does this not work? + # Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { return 'ignored' } + Mock -ModuleName $databaseModule -CommandName Get-ValidatedRuntimeParameter -MockWith { return 'ignored' } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + # Technically this relies on missing the value PackageId because it's an empty string + # This is really more of a hello-world test + Context "Missing parameters - PackageId" { + It "throws because a required parameter is not provided" { + { Invoke-AlkamiMigrationRunner -Runtime $runtime -PackageId "" -PackageVersion $packageVersion } | Should -Throw + } + } + + Context "Few parameters but enough" { + It "Seems to have enough parameters to work on developer machines" { + { Invoke-AlkamiMigrationRunner -Runtime $runtime -PackageId $packageId -PackageVersion $packageVersion } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + # Tom, why does this not work? + It "Calls Get-ValidatedRuntimeParameter" -Skip { + Assert-MockCalled -Module $databaseModule -CommandName Get-ValidatedRuntimeParameter -Times 1 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "A lot of parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -Runtime $runtime -PackageId $packageId -PackageVersion $packageVersion -ConnectionString $connectionString -DbmsUser $dbmsUser -SubscriptionHost $subscriptionhost -TempFolder $tempFolder -EnvironmentType $environmentType -BankGuids $bankGuids } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + # Tom, why does this not work? + It "Calls Get-ValidatedRuntimeParameter" -Skip { + Assert-MockCalled -Module $databaseModule -CommandName Get-ValidatedRuntimeParameter -Times 1 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } +} + +Describe "Invoke-AlkamiMigrationRunner_ORB" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { return "ignored" } + Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + Context "Invalid parameters - ConnectionString" { + It "throws" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "orb" -ConnectionString " " -OrbMigrateFolderPath "not a path" } | Should -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "orb" -ConnectionString $connectionString -OrbMigrateFolderPath $orbMigrateFolderPath } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "orb" -ConnectionString $connectionString -OrbMigrateFolderPath $orbMigrateFolderPath -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Calls Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 1 -Scope Context + } + } +} +Describe "Invoke-AlkamiMigrationRunner_GrantRoleTenants" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { return "ignored" } + Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + Context "Invalid parameters - ConnectionString" { + It "throws" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRoleTenants" -ConnectionString " " -DatabaseServiceAccount $databaseServiceAccount -TenantRoleName "blarg"} | Should -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRoleTenants" -ConnectionString $connectionString -DatabaseServiceAccount $databaseServiceAccount -TenantRoleName "blarg" } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRoleTenants" -ConnectionString $connectionString -DatabaseServiceAccount $databaseServiceAccount -TenantRoleName "blarg" -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRoleTenants" -ConnectionString $connectionString -DatabaseServiceAccount $databaseServiceAccount -TenantRoleName @("blarg","poof") -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Calls Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 1 -Scope Context + } + } +} +Describe "Invoke-AlkamiMigrationRunner_DbUserCreate" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { return "ignored" } + Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + Context "Invalid parameters - DataSource" { + It "throws" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "DbUserCreate" -DataSource " " -DatabaseName "not a DB" -DbmsUser "not a user" } | Should -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "DbUserCreate" -DataSource "not a server" -DatabaseName "not a DB" -DbmsUser "not a user" } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName "DbUserCreate" -DataSource "not a server" -DatabaseName "not a DB" -DbmsUser "not a user" -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Do not call Get-MigrationRunnerExe because we supplied the path" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 0 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Calls Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 1 -Scope Context + } + } +} +Describe "Invoke-AlkamiMigrationRunner_GrantRole" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Out-File -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-MigrationRunnerExe -MockWith { return "ignored" } + Mock -ModuleName $moduleForMock -CommandName Get-ValidatedRuntimeParameter -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return "some content for logging maybe" } + + Context "Invalid parameters - DataSource" { + It "throws" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRole" -DataSource " " -DatabaseName "not a DB" -DbmsUser "not a user" } | Should -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationTypeName "GrantRole" -DataSource "not a server" -DatabaseName "not a DB" -DbmsUser "not a user" } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Called Get-MigrationRunnerExe" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 1 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Does not call Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 0 -Scope Context + } + } + + Context "Validish parameters provided" { + It "does not throw because it seems to have valid parameters" { + { Invoke-AlkamiMigrationRunner -MigrationRunnerPath $MigrationRunnerPath -MigrationTypeName "GrantRole" -DataSource "not a server" -DatabaseName "not a DB" -DbmsUser "not a user" -LogFileFolder $logFileFolder } | Should -Not -Throw + } + It "Does not call Write-Warning" { + Assert-MockCalled -Module $moduleForMock -CommandName Write-Warning -Times 0 -Scope Context + } + It "Do not call Get-MigrationRunnerExe because we supplied the path" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-MigrationRunnerExe -Times 0 -Scope Context + } + It "Does not call Get-ValidatedRuntimeParameter (because we specified a valid value in the function)" { + Assert-MockCalled -Module $moduleForMock -CommandName Get-ValidatedRuntimeParameter -Times 0 -Scope Context + } + It "Called Invoke-CallOperatorWithPathAndParameters" { + Assert-MockCalled -Module $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -Times 1 -Scope Context + } + It "Calls Out-File" { + Assert-MockCalled -Module $moduleForMock -CommandName Out-File -Times 1 -Scope Context + } + } +} +Remove-Module -Name $sut.Replace(".ps1","") -Force \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-ExecuteQueryByConnectionString.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-ExecuteQueryByConnectionString.ps1 new file mode 100644 index 0000000..44f9f1e --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-ExecuteQueryByConnectionString.ps1 @@ -0,0 +1,190 @@ +function Invoke-ExecuteQueryByConnectionString { +<# + .SYNOPSIS + Executes a T-SQL script using a given connection string, using the ExecuteReader method + + .DESCRIPTION + Executes a T-SQL script using a given connection string, using the ExecuteReader method + This method will return one or more result sets according to the complexity of your query. Requesting multiple results will return multiple values. + + .PARAMETER ConnectionString + The database connection string. + + .PARAMETER QueryString + The query to execute + + .PARAMETER SqlInputParameters + An optional hashtable array of input parameters which may be passed to the script. Hashtable must be supplied with properties Name and Value + + .PARAMETER CommandTimeout + The query timeout in seconds + + .PARAMETER WhatIf + Run the query in a wrapped rollback transaction + + .PARAMETER Force + Allow potentially destructive queries to be run. Please do not use this except with approval from leadership. + + .EXAMPLE + $params = @( { Name = "@IsStolen"; Value = "1"; }) + > Invoke-ExecuteQueryByConnectionString -ConnectionString "data source=foo.local;Integrated Security=SSPI;Database=SuperMan2" -Query "SELECT Id, AccountId, Pennies, DateCreated FROM dbo.StolenPennies WHERE IsStolen = @IsStolen" -SqlInputParameters $params + < [ { Id = 1; AccountId = 12345; Pennies = 12; DateCreated = '1969-12-31 12:59:59'; } ] + #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Event Handler Requirement')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Sender is a Required Event Handler Parameter')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ConnectionString, + + [Parameter(Mandatory = $true)] + [Alias('Sql')] + [Alias('Query')] + [string]$QueryString, + + [Parameter(Mandatory = $false)] + [hashtable[]]$SqlInputParameters = @(), + + [Parameter(Mandatory = $false)] + [int]$CommandTimeout = 30, + + [Parameter()] + [switch]$WhatIf, + + [Parameter()] + [switch]$Force + ) + + $logLead = Get-LogLeadName + + if (-not $Force) { + if (-not (Assert-SqlQueryIsSafe -Query $QueryString)) { + Write-Warning "$logLead : Script was not run. Please resolve errors and re-run" + return + } + } + + if ($WhatIf) { + $QueryString = ConvertTo-WhatIfQuery -Query $QueryString + } + + $conn = New-Object System.Data.SqlClient.SqlConnection + + try { + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($ConnectionString) + + } catch [System.Management.Automation.MethodException] { + + Write-Warning "$logLead : Provided connection string [$ConnectionString] is invalid. Execution cannot continue." + return $null + } + + if (-not (Confirm-DatabaseAccess -ConnectionString $conStrBuilder.ToString())) { + + Write-Warning "$logLead : Unable to connect to the database specified in connection string [$ConnectionString]. Execution cannot continue." + return + } + + $conn.ConnectionString = $conStrBuilder.ToString() + Write-Verbose ("$logLead : Connecting to database with connection string {0}" -f $conStrBuilder.ToString()) + + $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { + + param($sender, $eventArgs) + + # Handle if it's an error or informational message + $hasErrors = ($null-ne $eventArgs.Errors -and $eventArgs.Count -gt 0) + $consoleColor = ([System.ConsoleColor]::Gray) + + if ($hasErrors) { + + # We only want to write this as an "error" if the severity is greater than 10. This aligns with SQL/SSMS + $maxErrorLevel = ($eventArgs.Errors | Measure-Object -Property Class -Maximum).Maximum + if ($maxErrorLevel -gt 10) { + + $consoleColor = ([System.ConsoleColor]::Red) + } + } + + Write-Host $eventArgs.Message -ForegroundColor $consoleColor + }; + + try { + + $conn.add_InfoMessage($handler); + $conn.FireInfoMessageEventOnUserErrors = $true + + $conn.Open() + $query = New-Object System.Data.SqlClient.SqlCommand($QueryString, $conn) + $query.CommandTimeout = $CommandTimeout + + if (-NOT (Test-IsCollectionNullOrEmpty $SqlInputParameters)) { + + foreach ($parameter in $SqlInputParameters) { + + Write-Verbose "$logLead : Adding parameter [$($parameter.Name)] with value [$($parameter.Value)] to the SqlCommand" + $query.Parameters.AddWithValue($parameter.Name, $parameter.Value) | Out-Null + } + } + + $reader = $query.ExecuteReader() + + $allResults = New-Object -TypeName "System.Collections.ArrayList" + do { + $currentResults = @() + $columns = @() + + # Collect all the column names once per result set so we can use those as property names + for ($i = 0; $i -lt $reader.FieldCount; $i++) { + try { + $columns += $reader.GetName($i) + } catch { + # Use a default value in the case of an error reading this column name (ex: unnamed columns being returned from the server) + $columns += "Column$i" + } + } + + # For each result in the result set ... bog standard ADO.NET code + while ($reader.Read()) { + $rowResult = @{} + for ($i = 0; $i -lt $reader.FieldCount; $i++) { + # This chunk of code will assign a variable to an object with the column name + # example: `select count(*) from table` returns an unnamed column, which gets a default name from SQL. If the column doesn't have a name, we default to Column1, Column2, etc (assigned above) + # example: `select count(*) as count from table` will return a column named count + # We would then name the result as $rowResult.count = $null or $rowResult.count = $reader[$i] + $columnName = $columns[$i] + + # IsDBNull is a special value that causes errors if not handled correctly + if ($reader.IsDBNull($i)) { + $rowResult[$columnName] = $null + } else { + # This will automatically cast to the correct type, but PS is also type-forgiving, so we just take the naive value + $rowResult[$columnName] = $reader[$i] + } + } + $currentResults += $rowResult + } + $allResults.Add($currentResults) | Out-Null + } while ($reader.NextResult()) + + return $allResults + + } catch { + + Write-Warning "$logLead : An exception occurred while trying to execute the specified query against the database" + Write-Warning "$logLead : $($_.ToString())" + Write-Warning "$logLead : $($_.ScriptStackTrace)" + return $null + + } finally { + + if (($null -ne $conn) -and ($conn.State -ne [System.Data.ConnectionState]::Closed)) { + + $conn.Close() + } + + $conn = $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-Migrate.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-Migrate.ps1 new file mode 100644 index 0000000..4d696f8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-Migrate.ps1 @@ -0,0 +1,26 @@ +function Invoke-Migrate { +<# +.SYNOPSIS + Invokes running the migrations using FluentMigrator +#> + [CmdletBinding()] + Param( + $connectionString, + $dbtag, + $migrationPath, + $context, + $version + ) + process{ + if(!$version) + { + $version = "0"; + } + Write-Verbose "& $(Join-Path (Get-MigrationRunnerPath) 'Migrate.exe') -conn '$connectionString' -provider 'sqlserver2008' -assembly '$migrationPath' -tag '$dbtag' -context '$context' -version '$version' --timeout=300 | ForEach-Object { if ($_ -match 'migrating') { Write-Host $_; } }"; + & (Join-Path (Get-MigrationRunnerPath) "Migrate.exe") -conn "$connectionString" -provider "sqlserver2008" -assembly "$migrationPath" -tag "$dbtag" -context "$context" -version "$version" --timeout=300 | ForEach-Object { if ($_ -match "migrating") { Write-Host $_; } } + if (! $?) { + Write-Error $_.Exception; + throw "migrate failed"; + } + } +} diff --git a/Modules/Alkami.PowerShell.Database/Public/Invoke-NonQueryByConnectionString.ps1 b/Modules/Alkami.PowerShell.Database/Public/Invoke-NonQueryByConnectionString.ps1 new file mode 100644 index 0000000..8e28f2f --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Invoke-NonQueryByConnectionString.ps1 @@ -0,0 +1,149 @@ +function Invoke-NonQueryByConnectionString { + + <# + .SYNOPSIS + Executes a T-SQL script using a given connection string, using the ExecuteNonQuery method + + .DESCRIPTION + Executes a T-SQL script using a given connection string, using the ExecuteNonQuery method + + .PARAMETER ConnectionString + The database connection string. + + .PARAMETER QueryString + The query to execute + + .PARAMETER SqlInputParameters + An optional hashtable array of input parameters which may be passed to the script. Hashtable must be supplied with properties Name and Value + + .PARAMETER CommandTimeout + The query timeout in seconds + + .PARAMETER WhatIf + Run the query in a wrapped rollback transaction + + .PARAMETER Force + Allow potentially destructive queries to be run. Please do not use this except with approval from leadership. + + .EXAMPLE + $params = @( { Name = "@IsStolen"; Value = "1"; }) + Invoke-NonQueryByConnectionString -ConnectionString "data source=foo.local;Integrated Security=SSPI;Database=SuperMan2" -Query "SELECT * FROM dbo.StolenPennies WHERE IsStolen = @IsStolen" -SqlInputParameters $params + #> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Event Handler Requirement')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Sender is a Required Event Handler Parameter')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ConnectionString, + + [Parameter(Mandatory = $true)] + [Alias('Sql')] + [Alias('Query')] + [string]$QueryString, + + [Parameter(Mandatory = $false)] + [hashtable[]]$SqlInputParameters = @(), + + [Parameter(Mandatory = $false)] + [int]$CommandTimeout = 30, + + [Parameter()] + [switch]$WhatIf, + + [Parameter()] + [switch]$Force + ) + + $logLead = Get-LogLeadName + + if (-not $Force) { + if (-not (Assert-SqlQueryIsSafe -Query $QueryString)) { + Write-Warning "$logLead : Script was not run. Please resolve errors and re-run" + return + } + } + + if ($WhatIf) { + $QueryString = ConvertTo-WhatIfQuery -Query $QueryString + } + + $conn = New-Object System.Data.SqlClient.SqlConnection + + try { + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($ConnectionString) + + } catch [System.Management.Automation.MethodException] { + + Write-Warning "$logLead : Provided connection string [$ConnectionString] is invalid. Execution cannot continue." + return $null + } + + if (-not (Confirm-DatabaseAccess -ConnectionString $conStrBuilder.ToString())) { + + Write-Warning "$logLead : Unable to connect to the database specified in connection string [$ConnectionString]. Execution cannot continue." + return + } + + $conn.ConnectionString = $conStrBuilder.ToString() + Write-Verbose ("$logLead : Connecting to database with connection string {0}" -f $conStrBuilder.ToString()) + + $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { + + param($sender, $eventArgs) + + # Handle if it's an error or informational message + $hasErrors = ($null-ne $eventArgs.Errors -and $eventArgs.Count -gt 0) + $consoleColor = ([System.ConsoleColor]::Gray) + + if ($hasErrors) { + + # We only want to write this as an "error" if the severity is greater than 10. This aligns with SQL/SSMS + $maxErrorLevel = ($eventArgs.Errors | Measure-Object -Property Class -Maximum).Maximum + if ($maxErrorLevel -gt 10) { + + $consoleColor = ([System.ConsoleColor]::Red) + } + } + + Write-Host $eventArgs.Message -ForegroundColor $consoleColor + }; + + try { + + $conn.add_InfoMessage($handler); + $conn.FireInfoMessageEventOnUserErrors = $true + + $conn.Open() + $query = New-Object System.Data.SqlClient.SqlCommand($QueryString, $conn) + $query.CommandTimeout = $CommandTimeout + + if (-NOT (Test-IsCollectionNullOrEmpty $SqlInputParameters)) { + + foreach ($parameter in $SqlInputParameters) { + + Write-Verbose "$logLead : Adding parameter [$($parameter.Name)] with value [$($parameter.Value)] to the SqlCommand" + $query.Parameters.AddWithValue($parameter.Name, $parameter.Value) | Out-Null + } + } + + $query.ExecuteNonQuery() | Out-Null + + } catch { + + Write-Warning "$logLead : An exception occurred while trying to execute the specified query against the database" + Write-Warning "$logLead : $($_.ToString())" + Write-Warning "$logLead : $($_.ScriptStackTrace)" + return $null + + } finally { + + if (($null -ne $conn) -and ($conn.State -ne [System.Data.ConnectionState]::Closed)) { + + $conn.Close() + } + + $conn = $null + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.ps1 b/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.ps1 new file mode 100644 index 0000000..1349482 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.ps1 @@ -0,0 +1,47 @@ +function Split-ConnectionString { + <# + .SYNOPSIS + Converts a connection string to a PowerShell object. + + .DESCRIPTION + Splits the fields of a specified connection string into a PowerShell object using SqlConnectionStringBuilder, which can also return default values. + + .PARAMETER ConnectionString + Required, the connection string to process. + + .PARAMETER Full + Not required. Returns full Connection String object details, including default values for properties not specified in the provided connection string data. + + .EXAMPLE + Split-ConnectionString -ConnectionString "data source=SL-Staging;Integrated Security=SSPI;Database=AlkamiMaster_Lane_PS1;MultiSubnetFailover=True" + + DataSource IntegratedSecurity InitialCatalog MultiSubnetFailover + ---------- ------------------ -------------- ------------------- + SL-Staging True AlkamiMaster_Lane_PS1 True + + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [String]$ConnectionString, + + [Parameter(Mandatory = $false)] + [Switch]$Full + ) + + $returnObject = @() + + $returnObject = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($ConnectionString) + + # return $returnObject.psbase rather than the raw object due to how powershell handles overrides of the property names (translating "Data Source" to "DataSource", etc) so expected, simple property names are returned (DataSource, InitialCatalog, etc). See https://stackoverflow.com/a/61903683 for more info. + + if ( $Full ) { + # if user requested all output, return it all + $returnObject.psbase + } else { + # otherwise just return the typical stuff + $returnObject.psbase | Select-Object DataSource,IntegratedSecurity,InitialCatalog,MultiSubnetFailover + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.tests.ps1 b/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.tests.ps1 new file mode 100644 index 0000000..27b7044 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Split-ConnectionString.tests.ps1 @@ -0,0 +1,126 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Split-ConnectionString" { + Context "Parameter Validation" { + It "Throws on Null ConnectionString" { + { Split-ConnectionString -ConnectionString $null } | Should Throw + } + + It "Throws on Invalid Connection String" { + { Split-ConnectionString -ConnectionString "Invalid Connection String" } | Should Throw + } + + It "Throws on Invalid Integrated Security Value" { + # this value is boolean, which behind the scenes can be converted from SSPI to true, or read as true/false, or empty as false + $testConnectionString = "data source=Test-DataSource;Integrated Security=INVALIDVALUE;Database=TestDatabase;MultiSubnetFailover=True" + { Split-ConnectionString -ConnectionString $testConnectionString } | Should Throw + } + + It "Throws on Invalid MultiSubnet Failover Value" { + $testConnectionString = "data source=Test-DataSource;Integrated Security=SSPI;Database=TestDatabase;MultiSubnetFailover=INVALIDVALUE" + { Split-ConnectionString -ConnectionString $testConnectionString } | Should Throw + } + } + + Context "Returns Correct Datatype" { + $testConnectionString = "data source=Test-DataSource;Integrated Security=SSPI;Database=TestDatabase;MultiSubnetFailover=True" + $shortTestConnectionString = "data source=Test-DataSource;Database=TestDatabase;" + + It "Returns Type PSCustomObject Without Full Parameter" { + { Split-ConnectionString -ConnectionString $testConnectionString } | Should -BeOfType [PSCustomObject] + } + + It "Returns Type PSCustomObject Without Full Parameter And With Minimal Connection String" { + { Split-ConnectionString -ConnectionString $shortTestConnectionString } | Should -BeOfType [PSCustomObject] + } + + It "Returns Type PSCustomObject With Full Parameter" { + { Split-ConnectionString -ConnectionString $testConnectionString -Full} | Should -BeOfType [PSCustomObject] + } + + It "Returns Type PSCustomObject With Full Parameter And With Minimal Connection String" { + { Split-ConnectionString -ConnectionString $shortTestConnectionString -Full } | Should -BeOfType [PSCustomObject] + } + + } + + Context "Returns Data Correctly Without Full Parameter" { + $testConnectionString = "data source=Test-DataSource;Integrated Security=SSPI;Database=TestDatabase;MultiSubnetFailover=True" + $return = Split-ConnectionString -ConnectionString $testConnectionString + + It "Returns DataSource" { + $return.DataSource | Should -Be "Test-DataSource" + } + + # Integrated Security is a boolean - set as either true or SSPI in connection string for same result + It "Returns IntegratedSecurity" { + $return.IntegratedSecurity | Should -BeTrue + } + + # Database in connection string converted to InitialCatalog + It "Returns InitialCatalog" { + $return.InitialCatalog | Should -Be "TestDatabase" + } + + It "Returns MultiSubnetFailover" { + $return.MultiSubnetFailover | Should -BeTrue + } + } + + Context "Returns Data Correctly With Minimal Connection String Without Full Parameter" { + $testConnectionString = "data source=Test-DataSource;Database=TestDatabase;" + $return = Split-ConnectionString -ConnectionString $testConnectionString + + It "Returns DataSource" { + $return.DataSource | Should -Be "Test-DataSource" + } + + # Integrated Security is a boolean - set as either true or SSPI in connection string for same result + It "Returns IntegratedSecurity" { + $return.IntegratedSecurity | Should -BeFalse + } + + # Database in connection string converted to InitialCatalog + It "Returns InitialCatalog" { + $return.InitialCatalog | Should -Be "TestDatabase" + } + + It "Returns MultiSubnetFailover" { + $return.MultiSubnetFailover | Should -BeFalse + } + } + + Context "Returns Data Correctly With Full Parameter" { + $testConnectionString = "data source=Test-DataSource;Integrated Security=SSPI;Database=TestDatabase;MultiSubnetFailover=True" + $return = Split-ConnectionString -ConnectionString $testConnectionString -Full + + It "Returns DataSource" { + $return.DataSource | Should -Be "Test-DataSource" + } + + # Integrated Security is a boolean - set as either true or SSPI in connection string for same result + It "Returns IntegratedSecurity" { + $return.IntegratedSecurity | Should -BeTrue + } + + # Database in connection string converted to InitialCatalog + It "Returns InitialCatalog" { + $return.InitialCatalog | Should -Be "TestDatabase" + } + + It "Returns MultiSubnetFailover" { + $return.MultiSubnetFailover | Should -BeTrue + } + + # validate more than the basic 4 properties are being returned. keys isn't working. + It "Returns PacketSize" { + $return.PacketSize | Should -BeOfType [Int32] + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Test-DatabaseExists.ps1 b/Modules/Alkami.PowerShell.Database/Public/Test-DatabaseExists.ps1 new file mode 100644 index 0000000..eb291b8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Test-DatabaseExists.ps1 @@ -0,0 +1,79 @@ +function Test-DatabaseExists { +<# + +.SYNOPSIS + This function confirms that the user running this script has access to the requested database at the provided connection string. + +.DESCRIPTION + This function confirms that the user running this script has access to the requested database at the provided connection string. + It uses the credentials of the connection string to connect. + + In order to test successfully you must have access to the master database to run queries for database existence + +.PARAMETER connectionString + [string] Used to test connection to the server. Will always set the initial catalog to master. + +.PARAMETER dbName + [string] Database name to check exists. If this parameter is not passed, will take the value of the database in the connection string. + +.INPUTS + Connection string to connect to the server, Database name to confirm exists + +.OUTPUTS + true or false according to existence of the database + +.EXAMPLE + Test-DatabaseExists + +Test-DatabaseExists +#> + [CmdletBinding()] + param( + [string]$connectionString, + + [string]$dbName + ) + process { + $logLead = (Get-LogLeadName) + + try + { + Write-Verbose "$logLead : Attempting to verify the connection to $connectionString" + + $sqlConnectionBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder "$connectionString" + + if ([string]::IsNullOrWhiteSpace($dbName)) { + $dbName = $sqlConnectionBuilder.InitialCatalog + } + + $sqlConnectionBuilder["Database"] = "master" + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionBuilder.ConnectionString + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = @" + select [Name] from sys.databases where [name]=@name; +"@ + $command.Parameters.AddWithValue("@Name",$dbName) | Out-Null + + $sqlConnection.Open() + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + + $returnedName = "" + if ($reader.Read()) { + $returnedName = $reader[0] + } + $reader.Dispose() + + $sqlConnection.Close() + + return ($returnedName -eq $dbName) + } + catch + { + Write-Error "$logLead : Can not connect to the specified database. Do you have approved access to the server?" + Write-Host $_.Exception.Message + throw "could not connect to the database!" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Database/Public/Test-TenantConfiguration.ps1 b/Modules/Alkami.PowerShell.Database/Public/Test-TenantConfiguration.ps1 new file mode 100644 index 0000000..315cf36 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/Public/Test-TenantConfiguration.ps1 @@ -0,0 +1,115 @@ +function Test-TenantConfiguration { + + <# + + .SYNOPSIS + This function tests the configuration in the tenant table of a master database + + .DESCRIPTION + This function confirms that the configuration in the tenant table is accurate, and the database can be reached using either individual + field data from the table or the connectionstring value. If no connection string is supplied to the master database, will attempt to + read it from the machine config. + + .PARAMETER MasterDatabaseConnectionString + An optional SQL connection string to the master database + + .INPUTS + Connection string to connect to the master database + + .OUTPUTS + Array of PSObjects containing bad tenant detail, or null + + .EXAMPLE + Test-TenantConfiguration + #> + + [CmdletBinding()] + [OutputType([PSObject[]])] + param( + [Parameter(Mandatory = $false)] + [string]$MasterDatabaseConnectionString = $null + ) + + $logLead = Get-LogLeadName + [array]$tenants = Get-FullTenantListFromServer -connectionString $MasterDatabaseConnectionString + $badTenants = @() + + foreach ($tenant in $tenants) { + + $localError = $false + $tenantName = $($tenant.Name) + [string[]]$configErrors = @() + $tenantConnectionStringFieldBuilder = $null + $tenantComputedConnectionStringBuilder = $null + Write-Host "$logLead : Validating $tenantName" + + try { + $tenantConnectionStringFieldBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $tenant.ConnectionString + + if (!(Confirm-DatabaseAccess -ConnectionString ($tenantConnectionStringFieldBuilder.ToString()) -ConnectionTimeout 2 -WarningAction SilentlyContinue)) { + Write-Host "$logLead : [$tenantName] : Could not connect to the tenant using the ConnectionString Field Value" + $localError = $true + $configErrors+="Unable to Connect Using Connection String" + } + } catch { + + Write-Host "$logLead : [$tenantName] : Tenant ConnectionString Field Value is Invalid. Error: $($_.Exception.Message)" + $localError = $true + $configErrors+="Invalid Connection String" + } + + if ($tenant.DataSource -ne $tenantConnectionStringFieldBuilder.DataSource) { + + Write-Host "$logLead : [$tenantName] : DataSource Field Does Not Match ConnectionString Field" + $localError = $true + $configErrors+="DataSource/ConnectionString Mismatch" + } + + if ($tenant.Catalog -ne $tenantConnectionStringFieldBuilder.InitialCatalog) { + + Write-Host "$logLead : [$tenantName] : Catalog Field Value Does Not Match ConnectionString Field Value" + $localError = $true + $configErrors+="Catalog/ConnectionString Mismatch" + } + + try { + + $tenantComputedConnectionStringBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder + $tenantComputedConnectionStringBuilder.'Data Source' = $tenant.DataSource + $tenantComputedConnectionStringBuilder.'Initial Catalog' = $tenant.Catalog + $tenantComputedConnectionStringBuilder.'Integrated Security' = $true + + if (!(Confirm-DatabaseAccess -ConnectionString $tenantComputedConnectionStringBuilder.ToString() -ConnectionTimeout 2 -WarningAction SilentlyContinue)) { + Write-Host "$logLead : [$tenantName] : Could not connect to the tenant using the DataSource Field and Catalog Field Values" + $localError = $true + $configErrors+="Unable to Connect Using DataSource and Catalog Values" + } + } catch { + + Write-Host "$logLead : [$tenantName] : Could Not Create a ConnectonString Using the Tenant DataSource and Catalog Field Values. Error: $($_.Exception.Message)" + $localError = $true + $configErrors+="Invalid DataSource or Catalog Values" + } + + if ($localError) { + + $badTenants += New-Object PsObject -Property @{ + Name = $tenant.Name + DataSource = $tenant.DataSource + Catalog = $tenant.Catalog + ConnectionString = $tenant.ConnectionString + Errors = $configErrors + } + } else { + + Write-Host "$logLead : $tenantName Tenant Configuration Validated Successfully" + } + } + + if ($badTenants.Count -gt 0) { + + Write-Warning "$logLead : $($badTenants.Count) Tenants have invalid configuration. Review and correct configuration errors" + } + + return ($badTenants) +} diff --git a/Modules/Alkami.PowerShell.Database/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.Database/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..1224501 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/tools/chocolateyInstall.ps1 @@ -0,0 +1,36 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; +} diff --git a/Modules/Alkami.PowerShell.Database/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.Database/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..a6bd815 --- /dev/null +++ b/Modules/Alkami.PowerShell.Database/tools/chocolateyUninstall.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.nuspec b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.nuspec new file mode 100644 index 0000000..fb36512 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.nuspec @@ -0,0 +1,32 @@ + + + + Alkami.PowerShell.IIS + $version$ + Alkami Platform Modules - PowerShell - IIS + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.IIS + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami IIS module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.psd1 b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.psd1 new file mode 100644 index 0000000..9188f8e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.psd1 @@ -0,0 +1,13 @@ +@{ + RootModule = 'Alkami.PowerShell.IIS.psm1' + ModuleVersion = '3.22.27' + GUID = 'dee986b9-f8b2-4119-a73f-7018d472a889' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing IIS, websites and general web site management.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Services','Alkami.PowerShell.Configuration' + FunctionsToExport = 'Get-DefaultWebsite','Get-DefaultWebsiteDefaultPath','Get-DynamicPortFromHostname','Get-IISAppPoolChildApplicationsCount','Get-IISServerManager','Get-IISSiteList','Get-IISSitesByPath','Get-KnownSkipWebAppNames','Get-KnownWCFServices','Get-SiteTempDirectoryPath','Install-AlkamiWebApplication','Install-Provider','Install-WebApplication','Install-WebExtension','Install-Widget','New-AdminWebBinding','New-AdminWebSite','New-AppTierApplicationPool','New-AppTierApplicationPools','New-AppTierHostFileEntries','New-AppTierWebApplications','New-ClientWebBinding','New-ClientWebSite','New-DefaultWebsite','New-IPSTSWebBinding','New-IPSTSWebSite','New-WebBinding','New-WebSite','New-WebTierWebApplications','Open-AlkamiSites','Optimize-DefaultWebsite','Ping-AlkamiWebSites','Ping-AlkamiWebSitesExtended','Remove-WebBinding','Repair-ValidIISPaths','Restart-AlkamiAppPool','Save-IISServerManagerChanges','Set-AlkamiWebAppPoolConfiguration','Set-AllAppPoolDefaults','Set-AppCommandPropertyOnAppPool','Set-ApplicationPoolExecutionAccount','Set-AppTierDefaultWebSite','Set-AppTierFolderAndFilePermissions','Set-MimeTypeValue','Set-ServerMIMETypes','Set-ServerResponseHeaders','Set-WebTierDefaultWebSite','Set-WebTierFolderAndFilePermissions','Start-IISAndServices','Start-IISAppPoolByName','Start-IISOnly','Stop-IISAndServices','Stop-IISAppPoolByName','Stop-IISOnly','Test-AppCommandPropertyExistsOnAppPool','Test-IISAppPoolByName','Test-InstallerUseSymlinkStrategy','Test-KnownWCFServicesResolvable','Test-ShouldInstallExceptionService','Test-WebBinding','Uninstall-Provider','Uninstall-WebApplication','Uninstall-WebExtension','Uninstall-Widget' + AliasesToExport = 'Configure-AppTierDefaultWebSite','Configure-WebTierDefaultWebSite','Create-AdminWebBinding','Create-AdminWebSite','Create-AppTierApplicationPools','Create-AppTierHostFileEntries','Create-AppTierWebApplications','Create-ClientWebBinding','Create-ClientWebSite','Create-IPSTSWebBinding','Create-IPSTSWebSite','Create-WebBinding','Create-WebSite','Create-WebTierWebApplications','Get-AlkamiWebAppPool','New-AlkamiWebAppPool' +} diff --git a/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.pssproj b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.pssproj new file mode 100644 index 0000000..aea2af9 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Alkami.PowerShell.IIS.pssproj @@ -0,0 +1,117 @@ + + + Debug + 2.0 + {487d1152-7729-4f75-a2cb-65f4823a8827} + Exe + MyApplication + MyApplication + Alkami.PowerShell.IIS + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.IIS") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/AlkamiManifest.xml b/Modules/Alkami.PowerShell.IIS/AlkamiManifest.xml new file mode 100644 index 0000000..adb6153 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.IIS + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.IIS/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.IIS/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..d4ead7e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Private/VariableDeclarations.ps1 @@ -0,0 +1,21 @@ +## TODO: Convert this module to use WebAdministration instead of the DLL below. +try { + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName +} +catch { + try { + # Just in case we have IIS Express and IIS loaded (dev machines) + [System.Reflection.Assembly]::LoadFile("C:\Windows\system32\inetsrv\Microsoft.Web.Administration.dll") + } catch { + # Do nothing in case this is a brand new server + Write-Warning "[Alkami.PowerShell.IIS] : Unable to Load Assembly Microsoft.Web.Administration. Some functions may not work as expected." + } +} + +try { + Import-Module WebAdministration +} +catch { + # Do nothing in case this is a brand new server + Write-Warning "[Alkami.PowerShell.IIS] : Unable to Load Module WebAdministration. Some functions may not work as expected." +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsite.ps1 new file mode 100644 index 0000000..cd7407b --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsite.ps1 @@ -0,0 +1,56 @@ +Function Get-DefaultWebsite { +<# +.SYNOPSIS + Get the default website (typically "Default Web Site") + +.DESCRIPTION + This function will try to find the first site with a binding protocol of http and bound to port 80 (against localhost, 127.0.0.1, or *:80). + If no site is found, will return a $null. In that case run New-DefaultWebsite + Recommendation is to run the result through Optimize-DefaultWebsite which will return the object as well. +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Justification="Array Consolidation is Acceptable")] + Param( + [Parameter(Mandatory=$false)] + [string]$DefaultWebSiteName = "Default Web Site" + ) + + $loglead = (Get-LogLeadName) + + $returnWebsite = Get-Website -Name $DefaultWebSiteName + + # TODO: This pattern is likely to break in the future as we add more and more "non-default" sites. + # The goal of this function is to not-break for SDK clients partner developers. + $definitelySkipSiteNames = @('Client','Admin','WebClient','WebClientAdmin','IPSTS','IP-STS','Orion','Eagle Eye','CoreDashboard') + + if ($null -eq $returnWebsite){ + $eligibileSites = (Get-ChildItem IIS:\Sites) + + $sites = @() + $potentialBindings = @("*:80:","127.0.0.1:80:","localhost:80:") + + foreach($site in $eligibileSites) { + if ($null -ne $definitelySkipSiteNames.Where({$_ -match $site.Name})) { + Write-Host "$logLead : Site name [$($site.Name)] matched a definite-skip pattern. Ignoring and moving to the next." + continue + } + $siteBindings = $site.Bindings.Collection + foreach($binding in $siteBindings) { + $potentialBinding = ($binding.bindingInformation -split ' ') + if (($binding.protocol -eq 'http') -and $null -ne ($potentialBindings -contains $potentialBinding)[0]) { + $sites += $site; + } + } + } + + ## Get the site with the lowest ID + $sites = @(($sites, @() -ne $null)[0] | Sort-Object ID) + + Write-Verbose "$logLead : Falling back to array lookup for final value. Site count: $($sites.Length). Taking first record: [$($sites[0])]" + $returnWebsite = $sites[0]; + } else { + Write-Verbose "$logLead : Found '$DefaultWebSiteName' already existed" + } + + return $returnWebsite; +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsiteDefaultPath.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsiteDefaultPath.ps1 new file mode 100644 index 0000000..1a0d78e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-DefaultWebsiteDefaultPath.ps1 @@ -0,0 +1,13 @@ +function Get-DefaultWebsiteDefaultPath { +<# +.SYNOPSIS + Get the path to the default folder for IIS - "Default Web Site" +#> + + [CmdletBinding()] + Param() + + ## Basically generate and return "c:\inetpub\wwwroot" but by using $env:SystemDrive I _hopefully_ work on future Linux with little modification. + return (Join-Path (Join-Path $env:SystemDrive "inetpub") "wwwroot"); +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-DynamicPortFromHostname.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-DynamicPortFromHostname.ps1 new file mode 100644 index 0000000..f4e16b1 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-DynamicPortFromHostname.ps1 @@ -0,0 +1,44 @@ +function Get-DynamicPortFromHostname { +<# +.SYNOPSIS + Used to convert a hostname to a dynamic port assignment. Mostly used to allow stacking multiple applications on one website hostname without breaking functionality on the same server +.PARAMETER Hostname + The hostname to derive a port for. +.OUTPUTS + Returns a single int that is between 5000 and 65535 for port assignment. +#> + [CmdletBinding()] + [OutputType([int])] + param ( + [Parameter(Mandatory = $true)] + $Hostname + ) + $logLead = (Get-LogLeadName) + $output = "" + + $websiteBytes = [System.Text.Encoding]::UTF8.GetBytes($Hostname) + $hashBytes = [System.Security.Cryptography.MD5]::Create().ComputeHash($websiteBytes) + for($i = 0; $i -lt $hashBytes.Length; $i++) { + $output += $hashBytes[$i].ToString("X2") + } + + # We only want the last 4 digits of the array, that can give us up to 5 digits once converted from Hex + $output = $output[-4..-1] -join '' + $outputPort = [System.Convert]::ToInt32($output, 16) + + # Why 65535? Because that's the maximum port number for TCP ports + # Honestly, if our array was FFFF it should only be 65535 anyways + # We converted from Hex at 4 chars, so it should be fine here + # But if something gets modified above, we could end up with bad data + # Better to be defensive just in case + if ($outputPort -gt 65535) { + $outputPort = $outputPort - 65535 + } + # Why 5000? Because we decided that was a good lower threshold + if ($outputPort -lt 5000) { + $outputPort = $outputPort + 5000 + } + + Write-Host "$logLead : Found result port of [$outputPort]" + return $outputPort +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-IISAppPoolChildApplicationsCount.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-IISAppPoolChildApplicationsCount.ps1 new file mode 100644 index 0000000..b638257 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-IISAppPoolChildApplicationsCount.ps1 @@ -0,0 +1,18 @@ +Function Get-IISAppPoolChildApplicationsCount { +<# +.SYNOPSIS + Returns a count of the attached applications (including any root sites) in use by an application pool in IIS. + +.PARAMETER $AppPoolName + [string] The name of the application pool in question +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$AppPoolName + ) + process { + ## Based off this information https://stackoverflow.com/a/20751426/109749 + return @(Get-WebConfigurationProperty "/system.applicationHost/sites/site/application[@applicationPool=`'$appPoolName`']" "machine/webroot/apphost" -name path).Count + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-IISServerManager.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-IISServerManager.ps1 new file mode 100644 index 0000000..3777058 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-IISServerManager.ps1 @@ -0,0 +1,21 @@ +function Get-IISServerManager { + + <# + .SYNOPSIS + Returns a new Microsoft.Web.Administration.ServerManager object + + .DESCRIPTION + Returns a new Microsoft.Web.Administration.ServerManager object. Used for mocking + + .EXAMPLE + $mgr = Get-IISServerManager + + .OUTPUTS + Microsoft.Web.Administration.ServerManager + #> + + [CmdletBinding()] + param() + + return (New-Object Microsoft.Web.Administration.ServerManager) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-IISSiteList.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-IISSiteList.ps1 new file mode 100644 index 0000000..c15a977 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-IISSiteList.ps1 @@ -0,0 +1,44 @@ +function Get-IISSiteList { + +<# +.SYNOPSIS + Returns a list of all Alkami sites in IIS + +.PARAMETER adminSitesOnly + [switch] Opens only Admin sites on the server. Cannot be used with the IncludeIPSTS parameter + +.PARAMETER returnIPSTS + [switch] IPSTS is excluded by default, but will be returned if this parameter is set. Cannot be used + with the AdminOnly parameter +#> + [CmdletBinding(DefaultParameterSetName = 'NoParameters')] + [CmdletBinding()] + Param( + [Parameter(ParameterSetName = 'AdminOnly', Mandatory = $true)] + [Parameter(Mandatory = $false)] + [Alias("AdminOnly")] + [switch]$adminSitesOnly, + + [Parameter(ParameterSetName = 'IncludeIPSTS', Mandatory = $true)] + [Parameter(Mandatory = $false)] + [Alias("IncludeIPSTS")] + [switch]$returnIPSTS + ) + + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + [Regex]$siteMatchRegex = ".*WebClient.*" + + if ($adminSitesOnly.IsPresent) { + + [Regex]$siteMatchRegex = ".*WebClientAdmin$" + + } elseif ($returnIPSTS.IsPresent) { + + [Regex]$siteMatchRegex = "(.*WebClient.*|.*IPSTS)" + } + + [array]$sites = $mgr.Sites | Where-Object {$_.Applications["/"].VirtualDirectories["/"].PhysicalPath -match $siteMatchRegex} + + return $sites +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-IISSitesByPath.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-IISSitesByPath.ps1 new file mode 100644 index 0000000..1fe0e6a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-IISSitesByPath.ps1 @@ -0,0 +1,32 @@ +Function Get-IISSitesByPath { +<# +.SYNOPSIS + Returns a list (array) of all site-objects from Get-IISSite where the PhysicalPath matches the string passed in + +.EXAMPLE + Get-IISSitesByPath -Path C:\Orb\WebClient\ + +.PARAMETER Path + the path to find sites that match +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Path + ) + process { + ## Oneliner: ((Get-ChildItem IIS:\Sites) | Where-Object { $_.PhysicalPath -eq 'c:\orb\WebClient' }) + + $siteList = @() + $allSites = @(Get-ChildItem IIS:\Sites) + + foreach ($oneSite in $allSites) { + + if ($oneSite.PhysicalPath -eq $Path) { + $siteList += $oneSite + } + } + + return $siteList + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-KnownSkipWebAppNames.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-KnownSkipWebAppNames.ps1 new file mode 100644 index 0000000..0942c0a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-KnownSkipWebAppNames.ps1 @@ -0,0 +1,17 @@ +function Get-KnownSkipWebAppNames { +<# +.SYNOPSIS + There are a set of WebApps we should skip for certain actions, like ExceptionService + This returns that list + +.NOTES + Adding a value to this will prevent it being installed in New-AppTierWebApplications + Adding a value to this list will prevent it being tested in Test-KnownWCFServicesResolvable + Adding a value to this list is considered deprecation of this web app for systemwide-generic invocations, but not for specific commands that target the named webapps +#> + [CmdletBinding()] + [OutputType([string[]])] + param() + + return @('ExceptionService') +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-KnownWCFServices.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-KnownWCFServices.ps1 new file mode 100644 index 0000000..48e7ae4 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-KnownWCFServices.ps1 @@ -0,0 +1,11 @@ +function Get-KnownWCFServices { +<# +.SYNOPSIS + Get the known WCF services from the internal value +#> + [CmdletBinding()] + [OutputType([object[]])] + param() + + return $global:appTierApplications +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Get-SiteTempDirectoryPath.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Get-SiteTempDirectoryPath.ps1 new file mode 100644 index 0000000..95d0c78 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Get-SiteTempDirectoryPath.ps1 @@ -0,0 +1,99 @@ +function Get-SiteTempDirectoryPath($siteOrAppName) { +<# +.SYNOPSIS + This function gets a website's temporary directory path +#> + +[CmdletBinding()] +[OutputType([string])] + + $AlkamiHashCode = @" + public class AlkamiHashCode + { + // https://stackoverflow.com/a/41575325/109749 ish + public static string GetDirectoryName(string id, string appname, string path) { + string v_app_name64b = AlkamiHashCode.GetStringHashCode(("/LM/W3SVC/" + id + "/" + appname + path).ToLower(System.Globalization.CultureInfo.InvariantCulture)).ToString("x", System.Globalization.CultureInfo.InvariantCulture); + return AlkamiHashCode.GetString64(v_app_name64b).ToString("x8") + "\\" + v_app_name64b; + } + + // System.Web.Util.StringUtil.GetStringHashCode() (System.Web.4.0.0.0) via System.Web.Hosting.ApplicationManager.CreateAppDomainWithHostingEnvironment(string appId, IApplicationHost appHost, HostingEnvironmentParameters hostingParameters) + internal unsafe static int GetStringHashCode(string s) + { + fixed(char* ptr = s){ + int num = 352654597; + int num2 = num; + int* ptr2 = (int*)ptr; + for (int i = s.Length; i > 0; i -= 4) + { + num = ((num << 5) + num + (num >> 27) ^ *ptr2); + if (i <= 2) + { + break; + } + num2 = ((num2 << 5) + num2 + (num2 >> 27) ^ ptr2[1]); + ptr2 += 2; + } + return num + num2 * 1566083941; + } + } + + // https://stackoverflow.com/a/41575325/109749 + internal static unsafe int GetString64(string s) + { + fixed (char* str = s) + { + int num3; + char* chPtr = str; + int num = 0x1505; + int num2 = num; + for (char* chPtr2 = chPtr; (num3 = chPtr2[0]) != '\0'; chPtr2 += 2) + { + num = ((num << 5) + num) ^ num3; + num3 = chPtr2[1]; + if (num3 == 0) + { + break; + } + num2 = ((num2 << 5) + num2) ^ num3; + } + return (num + (num2 * 0x5d588b65)); + } + } + } +"@; + + $cp = [System.CodeDom.Compiler.CompilerParameters]::new($assemblies); + $cp.CompilerOptions = '/unsafe'; + Add-Type -TypeDefinition $AlkamiHashCode -CompilerParameters $cp; + + $rootOrAppName = "root"; + $physicalPath = ""; + $siteId = ""; + + $website = (Get-Website $siteOrAppName); + if ($null -ne $website) { + $physicalPath = $website.PhysicalPath.TrimEnd("\") + "\"; + $siteId = $website.id; + } else { + #This part does not work as expected just yet + $appPool = (Get-WebApplication $siteOrAppName); + if ($null -eq $appPool) { + Write-Warning "could not match to any site or apppool for $siteOrAppName" + return ""; + } else { + $sitename = $appPool.GetParentElement().attributes["name"].Value; + $website = (Get-Website $sitename); + if ($null -ne $website) { + $physicalPath = $website.PhysicalPath.TrimEnd("\") + "\"; + $siteId = $website.id; + $rootOrAppName = $siteOrAppName + } else { + Write-Warning "Could not find the site for $siteOrAppName"; + return ""; + } + } + } + + $tempFilePath = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\$rootOrAppName\" + [AlkamiHashCode]::GetDirectoryName($siteid, $rootOrAppName, $physicalPath); + return $tempFilePath +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.ps1 new file mode 100644 index 0000000..f7e088f --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.ps1 @@ -0,0 +1,435 @@ +Function Install-AlkamiWebApplication { +<# +.SYNOPSIS + Install a Web Application to the appropriate place + +.DESCRIPTION + Install a Web Application to the appropriate place. + Will ensure appropriate app pool exists + +.PARAMETER WebAppName + [string] The name of the web application. + +.PARAMETER SourcePath + [string] The folder that contains the files. Typically a chocolatey path. + +.PARAMETER IsClient + [switch] Is this package installed to client? + +.PARAMETER IsAdmin + [switch] Is this package installed to admin? + +.PARAMETER IsLegacy + [switch] Is this package installed to the legacy site? (typically Default Web Site) + +.PARAMETER NoManagedCode + [switch] Is this .net core, or Managed code? + +.PARAMETER AppPoolName + [switch] App pool name. Required for .net core apps. That is, if -NoManagedCode is included. + +.INPUTS + WebAppName and SourcePath are required. + Requires one of IsClient or IsAdmin or IsLegacy + +.OUTPUTS + Various diagnostic information about the install process + +.EXAMPLE + Install-AlkamiWebApplication -WebAppName BankService -SourcePath C:\Orb\BankService -IsLegacy + +Various diagnostic information about the install process. + +#> + ## We could define a named set here as the default, but I would rather not. Let it fail if we don't pass in the param flag + [CmdletBinding(DefaultParameterSetName='IsClient')] + [OutputType([System.Collections.ArrayList])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("IsClient", '', Justification="This is a Switch that goes with a parameter set. Function usage is confusing without this switch.", Scope = "Function")] + Param( + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true, Position=0)] + [string]$WebAppName, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=1)] + [Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=1)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=1)] + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true, Position=1)] + [string]$SourcePath, + [Parameter(ParameterSetName='IsClient',Mandatory=$true)] + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)] + [switch]$IsClient, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true)] + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)] + [switch]$IsAdmin, + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true)] + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)] + [switch]$IsLegacy, + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true)] + [switch]$NoManagedCode, + [Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true)] + [string]$AppPoolName + ) + process { + + $logLead = (Get-LogLeadName) + +#region Invoke-CommandWithRetry subdefinitions + $icwrSeconds = 3 # 3 seconds between retries + $icwrJitterMin = 0 # 0 milliseconds, so we wait a minimum of $icwrSeconds before retrying + $icwrJitterMax = 5000 # 5000 milliseconds, so 5 seconds + $icwrRetryCount = 5 # 5 retries, between 3s and 8s each, for as many as 40s to complete. + + #build an argument splat to save copy-paste readability + $icwrSplat = @{ + Seconds = $icwrSeconds + JitterMin = $icwrJitterMin + JitterMax = $icwrJitterMax + MaxRetries = $icwrRetryCount + } + + $setEnabledProtocolsScriptBlock= { + param ($sb_WebAppName, $sb_sitePath) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(enabledProtocols)" + (Set-ItemProperty "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name enabledProtocols -Value "http,net.tcp") | Out-Null + } + + $setApplicationPoolScriptBlock = { + param ($sb_WebAppName, $sb_sitePath, $sb_webAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(applicationPool)" + (Set-ItemProperty -Path "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name applicationPool -Value $sb_webAppPoolName) | Out-Null + } + + $setPreloadEnabledScriptBlock = { + param ($sb_WebAppName, $sb_sitePath) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(preloadEnabled)" + (Set-ItemProperty "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name preloadEnabled -Value "true") | Out-Null + } + + + # Used to modify .net CLR version to support .net core. + $setNoManagedCodeScriptBlock = { + param ($sb_webAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(managedRuntimeVersion)" + (Set-ItemProperty "IIS:\AppPools\$sb_webAppPoolName" -Name managedRuntimeVersion -Value "") | Out-Null + } + + $setAlkamiWebAppPoolConfigurationScriptBlock = { + param ($sb_webAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-AlkamiWebAppPoolConfiguration" + (Set-AlkamiWebAppPoolConfiguration $sb_webAppPoolName) | Out-Null + } + + $newAlkamiWebAppPoolScriptBlock = { + param ($sb_webAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-AlkamiWebAppPool" + return New-AlkamiWebAppPool -Name $sb_webAppPoolName + } + + $newWebApplicationScriptBlock = { + param ($sb_WebAppName, $sb_sitePath, $sb_appPhysicalPath, $sb_webAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-WebApplication" + # Use -Force to ensure it gets created correctly + return New-WebApplication -Name $sb_WebAppName -Site $sb_sitePath -PhysicalPath $sb_appPhysicalPath -ApplicationPool $sb_webAppPoolName -Force + } + + # value is not consumed currently, so not returned here + $newWebVirtualDirectoryScriptBlock = { + param ($sb_sitePath, $sb_folder, $sb_emptyPath) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-WebVirtualDirectory" + New-WebVirtualDirectory -Site $sb_sitePath -Name $sb_folder -PhysicalPath $sb_emptyPath + } + + # not doing out-null so we can see it print in the logs that it got deleted + $removeWebApplicationScriptBlock = { + param ($sb_WebAppName, $sb_sitePath) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Remove-WebApplication" + Remove-WebApplication $sb_WebAppName -Site $sb_sitePath + } + + # not doing out-null so we can see it print in the logs that it got deleted + $RemoveWebAppPoolScriptBlock = { + param ($sb_oldAppPoolName) + Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Remove-WebAppPool" + Remove-WebAppPool -Name $sb_oldAppPoolName + } +#endregion Invoke-CommandWithRetry subdefinitions + + if (!(Test-Path $SourcePath)) { + Write-Warning "$logLead : The path $SourcePath was not found, application $WebAppName will not be created" + return + } + + ## validate path points to the choco folder unless it is a legacy application + if ($IsLegacy -and !($sourcePath.ToLower().Contains((Get-OrbPath).ToLower()))) { + Write-Warning "$logLead : $WebAppName is marked IsLegacy but $SourcePath doesn't seem to be in the expected ORB folder. This will be an Alkami.IOC issue." + } elseif (!$IsLegacy) { + $isPackagePathValidated = (Test-PathIsInApprovedPackageLocation $sourcePath) + if (!($isPackagePathValidated)) { + Write-Warning "$logLead : The source location at [$sourcePath] doesn't match the expected criteria of non-IsLegacy installs" + Write-Warning "$logLead : The expectation is that non-IsLegacy installs will be constrained to the Chocolatey lib folder install path" + #throw "can not finish install - bad path provided" + } + } + + $siteList = @() + + $webAppPoolName = $WebAppName.Replace('\','_').Replace('/','_') + $parentAppName = $webAppPoolName + $newRelicAppName = $webAppPoolName + + $forceAppPoolsOnClientToUseWebClient = $false + + if ($IsLegacy) { + ## Use of this function ensures that the Default Web Site exists + $defaultWebsite = (Get-DefaultWebsite) + if ($null -eq $defaultWebsite) { + $defaultWebsite = Invoke-CommandWithRetry -ScriptBlock { return (New-DefaultWebsite) } @icwrSplat + } + ## Ensure we add this to the list of sites we are going to be installing to + $siteList += $defaultWebsite + + $parentAppName = $defaultWebsite.Name + } else { + ## Test to make sure this is the valid site name. We can do this by looking for the site, if it doesn't exist, look for the path + ## C:\Orb\$parentAppName and then find any sites for that. + + ## By process of elimination, if you weren't in parameter set IsAdmin or IsLegacy, you must be in IsClient. + $parentAppName = 'WebClient' + + if ($IsAdmin) { + $parentAppName = 'WebClientAdmin' + } + + # SRE-16828 - If you want to force apps to be under WebClient or WebClientAdmin this is the line to change + $forceAppPoolsOnClientToUseWebClient = $true + + $targetFolderPath = (Join-Path (Get-OrbPath) $parentAppName) + + $siteList = (Get-IISSitesByPath $targetFolderPath) + } + + if (Test-IsCollectionNullOrEmpty $siteList) { + Write-Warning "$logLead : Can't find any sites to bind to for WebAppName: [$WebAppName] and ParentAppName: [$parentAppName]" + throw "$logLead : Can't find any sites to bind to for WebAppName: [$WebAppName] and ParentAppName: [$parentAppName]" + } else { + Write-Host "$logLead : Found these sites [`"$(($siteList).Name -join '`",`"')`"]" + } + + # SRE-16828 - If you want to force apps to be under WebClient or WebClientAdmin this is the line to change + if ($forceAppPoolsOnClientToUseWebClient) { + $webAppPoolName = $ParentAppName + } + + # NoManagedCode (that is, .net core apps) can't run on the same app pool as any other app, as opposed to ManagedCode that can + if($NoManagedCode){ + $webAppPoolName = $AppPoolName + } + + ## Get the web app pool to make sure that's configured correctly before we start looping sites + $appPoolPath = (Join-Path "IIS:\AppPools" $webAppPoolName) + $appPool = (Get-Item $appPoolPath -ErrorAction SilentlyContinue) + + ## If we couldn't find the application pool for this app, need to create one to go with this application + ## Else we need to ensure the app pool is properly configured + if ($null -eq $appPool) { + $icwrArguments = @{ + ScriptBlock = $newAlkamiWebAppPoolScriptBlock + Arguments = @($webAppPoolName) + } + $appPool = Invoke-CommandWithRetry @icwrArguments @icwrSplat + } else { + if (!$forceAppPoolsOnClientToUseWebClient) { + $icwrArguments = @{ + ScriptBlock = $setAlkamiWebAppPoolConfigurationScriptBlock + Arguments = @($webAppPoolName) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + } + + # Set managedRuntimeVersion to noManagedCode for .net core. + # This *ALWAYS* has to go after Set-WebPoolConfig + if($NoManagedCode){ + Write-Host "$logLead : Setting Application Attribute .NET CLR Version to '' (noManagedCode) for AppPools\$WebAppPoolName" + $icwrArguments = @{ + ScriptBlock = $setNoManagedCodeScriptBlock + Arguments = @($WebAppPoolName) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + + $oldAppPoolNames = @() + + foreach ($site in $siteList) { + $sitePath = $site.Name + + $virtualFolders = [System.Collections.ArrayList]($WebAppName.Split(@('/','\'))) + + ## Foreach virtual path in our segmented list, we need to ensure we have a folder on disk to point to + ## By having an empty folder when one doesn't exist, there can't be much data leakage, as there's no child folders + ## Still a risk of ..\ links in whatever might get misconfigured, so we just need to be diligent + if ($virtualFolders.Count -gt 1) { + $depthCount = $virtualFolders.Count - 1 + $WebAppName = $virtualFolders[-1] + + $emptyPath = (Join-Path (Get-OrbPath) "Empty") + if (!(Test-Path $emptyPath)) { + (New-Item -Path $emptyPath -ItemType Directory -Force) | Out-Null + $emptyPathReadmePath = (Join-Path $emptyPath "readme.txt") + if(!(Test-Path $emptyPathReadmePath)) { + Set-Content -Path $emptyPathReadmePath -Value @" +This folder should remain empty except this file. + +If this folder has directory browsing turned on, it should be turned off. + +Please report any instance of this directory browsing turned on to security@alkamitech.com along with the URL where you found it. + +Thank you for your assistance. +"@ + } + } + for ($i = 0; $i -lt $depthCount; $i++) { + $folder = $virtualFolders[$i] + ## Recursively create subfolders under the site. + ## See https://stackoverflow.com/a/21187565/109749 + if ($null -eq (Get-WebVirtualDirectory -Site $sitePath -Name $folder)) { + $icwrArguments = @{ + ScriptBlock = $newWebVirtualDirectoryScriptBlock + Arguments = @($sitePath, $folder, $emptyPath) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + + ## Keep appending the folder we just created to the existing $sitePath + ## This gets injected below in the middle of the string + $sitePath = $sitePath + "\" + $folder + } + } + + $appPhysicalPath = $SourcePath + $appPhysicalPathContent = (Join-Path $appPhysicalPath Content) + ## Try to find the folder at c:\programdata\choco\lib\myapp\Content\App which should have views, images, etc in it. + if (Test-Path $appPhysicalPathContent) { + $appPhysicalPathContentTest = (Join-Path $appPhysicalPathContent 'App') + if (Test-Path $appPhysicalPathContentTest) { + $appPhysicalPath = $appPhysicalPathContentTest + } else { + ## Try to find the folder at c:\programdata\choco\lib\myapp\Content\* which has files in it, guessing that's what we want instead, even though it breaks the pattern + if (Test-Path $appPhysicalPathContent) { + $appPhysicalPathContentTest = (Get-ChildItem $appPhysicalPathContent) | Where-Object { $_.PSIsContainer } | Select-Object -First 1 + if (($null -ne $appPhysicalPathContentTest) -and ($null -ne (Get-ChildItem $appPhysicalPathContentTest))) { + $appPhysicalPath = $appPhysicalPathContentTest + } elseif ($null -ne (Get-ChildItem $appPhysicalPathContent)) { + ## Sigh, just use the c:\programdata\choco\lib\myapp\Content cos there's stuff in there, who knows anymore, life is bleak + $appPhysicalPath = $appPhysicalPathContent + } + } + } + } + + $existingApp = Get-WebApplication $WebAppName -Site $sitePath + + if ($null -ne $existingApp) { + ## Ensure the path matches the expected location in case it has moved, see SRE-12829 + if ($existingApp.PhysicalPath -ne $appPhysicalPath) { + $icwrArguments = @{ + ScriptBlock = $removeWebApplicationScriptBlock + Arguments = @($WebAppName, $sitePath) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + $existingApp = $null ## force it to null so the next value kicks in + } + } + + $existingApp = Get-WebApplication $WebAppName -Site $sitePath + + if ($null -eq $existingApp) { + Write-Host "Creating [$WebAppName] for [$sitePath]" + $icwrArguments = @{ + ScriptBlock = $newWebApplicationScriptBlock + Arguments = @($WebAppName, $sitePath, $appPhysicalPath, $webAppPoolName) + } + $existingApp = Invoke-CommandWithRetry @icwrArguments @icwrSplat + } else { + Write-Host "Found existing Web Application for [$WebAppName] on [$sitePath]" + } + + ## If the name of the application pool matches the one that's on the existing webapplication then groovy + ## Otherwise we have to re-point the web-app to use the app pool with the same name. + ## We already made sure we have such an app pool that matches the name, so we just need to do the attaching. + if ( $existingApp.applicationPool -ne $webAppPoolName) { + ## We have a valid app pool that uses the same name as the web app, and we are here because they didn't match. + + ## Random bug catch, eep + if (![string]::IsNullOrWhiteSpace($existingApp.applicationPool)) { + ## Store the old name so we can delete any unused app pools later + Write-Verbose "Adding existing application pool that doesn't match the expected name to the delete-list $($existingApp.applicationPool)" + $oldAppPoolNames += $existingApp.applicationPool + } + + ## Let's assign the new app pool to the web application + $icwrArguments = @{ + ScriptBlock = $setApplicationPoolScriptBlock + Arguments = @($WebAppName, $sitePath, $webAppPoolName) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + + if ($IsLegacy) { + Write-Host "$logLead : Setting Application Attribute EnabledProtocols [http,net.tcp] for $sitePath\$WebAppName" + $icwrArguments = @{ + ScriptBlock = $setEnabledProtocolsScriptBlock + Arguments = @($WebAppName, $sitePath) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + + Write-Host "$logLead : Setting Application Attribute preloadEnabled to True for $sitePath\$WebAppName" + $icwrArguments = @{ + ScriptBlock = $setPreloadEnabledScriptBlock + Arguments = @($WebAppName, $sitePath) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } + + $environmentUserPrefix = (Get-AppSetting "Environment.Name" -SuppressWarnings) + + if ($null -ne $environmentUserPrefix) { + ## Find the config file path. This is a web app, so we default to web.config as the file name and in the same root folder as the content + ## If we had a non-web-app we might have to look for another file but this is Install-AlkamiWebApplication. + $ConfigFilePath = (Join-Path $appPhysicalPath "web.config") + + if (!(Test-Path $ConfigFilePath)) { + Write-Warning "Can't find a web.config under the folder [$ConfigFilePath]" + } + else { + ## ensure NR name set in web.config with Environment.Name + AppName from config + ## This function will get the application label for us so we don't have to manually fetch it + Set-NewRelicAppNameConfigFileValue $environmentUserPrefix $ConfigFilePath $newRelicAppName + } + } + + foreach($oldAppPoolName in $oldAppPoolNames) { + ## The same "wrong" app pool could have been used on other apps so only remove them if there are no referenced apps for this pool + $appPoolPath = (Join-Path "IIS:\AppPools" $oldAppPoolName) + $existingApp = (Get-Item $appPoolPath -ErrorAction SilentlyContinue) + + if ($null -ne $existingApp) { + $existingAppApplicationCount = (Get-IISAppPoolChildApplicationsCount $oldAppPoolName) + + ## The count of that outdated application pool is 0, indicating that apppool isn't in use. Delete it. + if ($existingAppApplicationCount -eq 0) { + Write-Verbose "$logLead : Removing identified and unused Web AppPool [$oldAppPoolName]" + $icwrArguments = @{ + ScriptBlock = $RemoveWebAppPoolScriptBlock + Arguments = @($oldAppPoolName) + } + Invoke-CommandWithRetry @icwrArguments @icwrSplat + } else { + Write-Verbose "$logLead : Web AppPool [$oldAppPoolName] still has things attached to it. Not deleting." + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.tests.ps1 new file mode 100644 index 0000000..5ba8d21 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-AlkamiWebApplication.tests.ps1 @@ -0,0 +1,329 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Install-AlkamiWebApplication" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return "this is a garbage value" } + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-DefaultWebsite -MockWith { return @{} } + Mock -ModuleName $moduleForMock -CommandName Get-IISAppPoolChildApplicationsCount -MockWith { return 0 } + Mock -ModuleName $moduleForMock -CommandName Get-IISSitesByPath -MockWith { return @{} } + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return @{} } + Mock -ModuleName $moduleForMock -CommandName Get-OrbPath -MockWith { return "TestDrive:\Orb" } + Mock -ModuleName $moduleForMock -CommandName Get-WebApplication -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-AlkamiWebAppPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-DefaultWebsite -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-WebApplication -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-WebVirtualDirectory -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Remove-WebAppPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Remove-WebApplication -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Set-Content -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Set-ItemProperty -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Set-NewRelicAppNameConfigFileValue -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Test-IsCollectionNullOrEmpty -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-PathIsInApprovedPackageLocation -MockWith { return $true } + + $fakeAppName = 'FakeApp' + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + + Context "When Testing Parameter Sets" { + # Test that parameter sets function correctly + It "Handles Managed Code Correctly" { + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsLegacy } | Should -Not -Throw + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient } | Should -Not -Throw + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsAdmin } | Should -Not -Throw + } + + It "Handles No Managed Code Correctly"{ + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsLegacy -NoManagedCode -AppPoolName "fake.App.Pool" } | Should -Not -Throw + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient -NoManagedCode -AppPoolName "fake.App.Pool" } | Should -Not -Throw + { Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsAdmin -NoManagedCode -AppPoolName "fake.App.Pool" } | Should -Not -Throw + } + } + +<# + $canRunIntegrationTests = $false + + try { + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName + + if (Test-IsAdmin) { + $canRunIntegrationTests = $true + } else { + Write-Warning "Process Not Running as Admin. Integration Tests Will Not Be Executed" + $canRunIntegrationTests = $false + } + } catch { + # If IIS Isn't Installed, We Can't Actually Test This in IIS. Only Unit tests will run + Write-Warning "Unable to Load Microsoft.Web.Administration. Integration Tests Will Not Be Executed" + $canRunIntegrationTests = $false + } + + Context "Unit Tests" { + + It "Ensure warning returned if no path valid provided" { + + $fakePath = "Obviously this is not a path and it can't pretend to be a path" + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsLegacy + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + + } + + Context "Integration Tests on -IsLegacy" { + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + It "[Integration] Ensure dummy app is found after creating on legacy" -Skip:(!$canRunIntegrationTests) { + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsLegacy + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Remove-WebApplication -Site "Default Web Site" -Name $fakeAppName + Remove-WebAppPool -Name $fakeAppName + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + + It "[Integration] Ensure dummy app is found after creating on legacy and removing with Uninstall-WebApplication" -Skip:(!$canRunIntegrationTests) { + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { $true } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsLegacy + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Uninstall-WebApplication -WebAppName $fakeAppName -IsLegacy + + Get-WebApplication -Site "Default Web Site" -Name $fakeAppName | Should Be $null + Get-IISAppPool -Name $fakeAppName | Should Be $null + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + } + + Context "Integration Tests on -IsClient" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + It "[Integration] Ensure dummy app is found after creating on client" -Skip:(!$canRunIntegrationTests) { + + if ($null -eq (Get-IISAppPool 'WebClient')) { + Set-ItResult -Inconclusive -Because "It would appear that IIS or WebAdmin are found, but there is no app pool called WebClient" + return + } + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Remove-WebApplication -Site "WebClient" -Name $fakeAppName + Remove-WebAppPool -Name $fakeAppName + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + + It "[Integration] Ensure dummy app is found after creating on client and removing with Uninstall-WebApplication" -Skip:(!$canRunIntegrationTests) { + + if ($null -eq (Get-IISAppPool 'WebClient')) { + Set-ItResult -Inconclusive -Because "It would appear that IIS or WebAdmin are found, but there is no app pool called WebClient" + return + } + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { $true } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Uninstall-WebApplication -WebAppName $fakeAppName -IsClient + + Get-WebApplication -Site "WebClient" -Name $fakeAppName | Should Be $null + Get-IISAppPool -Name $fakeAppName | Should Be $null + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + } + + Context "Integration Tests on -IsAdmin" { + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + + It "[Integration] Ensure dummy app is found after creating on Admin" -Skip:(!$canRunIntegrationTests) { + + if ($null -eq (Get-IISAppPool 'WebClientAdmin')) { + Set-ItResult -Inconclusive -Because "It would appear that IIS or WebAdmin are found, but there is no app pool called WebClientAdmin" + return + } + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsAdmin + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Remove-WebApplication -Site "WebClientAdmin" -Name $fakeAppName + Remove-WebAppPool -Name $fakeAppName + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + + It "[Integration] Ensure dummy app is found after creating on admin and removing with Uninstall-WebApplication" -Skip:(!$canRunIntegrationTests) { + + if ($null -eq (Get-IISAppPool 'WebClientAdmin')) { + Set-ItResult -Inconclusive -Because "It would appear that IIS or WebAdmin are found, but there is no app pool called WebClientAdmin" + return + } + + + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $createdSourcePath = $false + if (!(Test-Path $fakePath)) { + $createdSourcePath = $true + New-Item -ItemType Directory -Path $fakePath + } + + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { $true } + + Install-AlkamiWebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsAdmin + + $Attributes = (Get-IISAppPool $fakeAppName).Attributes + + $Attributes["queueLength"].Value | Should Be 5000 + $Attributes["autoStart"].Value | Should Be $true + $Attributes["enable32BitAppOnWin64"].Value | Should Be $false + $Attributes["managedRuntimeVersion"].Value | Should Be 'v4.0' + $Attributes["managedPipelineMode"].Value | Should Be 0 + $Attributes["startMode"].Value | Should Be 1 + + Uninstall-WebApplication -WebAppName $fakeAppName -IsAdmin + + Get-WebApplication -Site "WebClientAdmin" -Name $fakeAppName | Should Be $null + Get-IISAppPool -Name $fakeAppName | Should Be $null + + if ($createdSourcePath) { + Remove-Item $fakePath + } + } + } + + if ($canRunIntegrationTests) { + $userPath = (Get-UsersPath $fakeAppName) + $testPath = (Join-Path IIS:\AppPools $fakeAppName) + Write-Host "removing $fakeAppName" + if (Test-Path $testPath) { + Remove-Item IIS:\AppPools\$fakeAppName -Force + } + Write-Host $userPath + if (Test-Path $userPath) { + @(@(Get-CimInstance -ClassName Win32_UserProfile).Where({$_.LocalPath -eq $userPath})) | Remove-CimInstance + if (Test-Path $userPath) { + Write-Host "path exists" + Remove-Item $userPath -Force -Recurse + } + } + } + #> +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-Provider.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-Provider.ps1 new file mode 100644 index 0000000..4c88a6c --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-Provider.ps1 @@ -0,0 +1,331 @@ +function Install-Provider { +<# +.SYNOPSIS + This function is used in conjunction with Alkami.Installer.Provider to install legacy providers to the appropriate locations. + +.DESCRIPTION + This function is used in conjunction with Alkami.Installer.Provider to install legacy providers to the appropriate locations. + It will stop the following services and restart them if they were running at the time of install: + * BankService - WCF Service + * SchedulerService - WCF Service + * CoreService - WCF Service + * SecurityManagementService - WCF Service + * NagConfigurationService - WCF Service + * Nag - Windows Service + * Radium - Windows Service + +.PARAMETER ProviderAssemblyInfo + The provider's configured assembly info. + +.PARAMETER SourcePath + This is the path to the source containing folder. Typically this is a chocolatey folder. + +.PARAMETER ProviderName + The provider's configured name. This is not required, it will be used for display purposes only as of Alkami.PowerShell.IIS v3.1.1 + +.PARAMETER ProviderTargets + Names of services that the Provider will be installed to + +.PARAMETER NoSymlink + Force installation using File Copy operations, omitting a call to Test-InstallerUseSymlinkStrategy, even on hosts with the ENVIRONMENT VARIABLE 'Alkami.Installer.UseSymlink' set + +.LINK + Test-InstallerUseSymlinkStrategy + +.OUTPUTS + This function will only Write-Information, but also show the files that will be tentatively copied over + +.EXAMPLE + Install-Provider -ProviderAssemblyInfo Alkami.App.Providers.Core.Dynamic -SourcePath C:\ProgramData\chocolatey\lib\Alkami.App.Providers.Core.Dynamic + +Install-Provider output may be verbose from the underlying calls being made. + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [AllowNull()] + [string]$ProviderAssemblyInfo, + + [Parameter(Mandatory = $true)] + [ValidateScript( { Test-Path (Resolve-Path $_) })] + [string]$SourcePath, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string]$ProviderName = $null, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string[]]$ProviderTargets = @('BankService', 'CoreService', 'NotificationService', 'SecurityManagementService', 'Radium', 'Nag', 'NagConfigurationService', 'SchedulerService'), + + [Parameter(Mandatory = $false)] + [switch]$NoSymlink + ) + process { + $loglead = Get-LogLeadName + + Write-Host "$loglead : $ProviderName being installed by $($env:USERNAME) on $($env:COMPUTERNAME) at $(Get-Date)" + + ## If it's a developer machine we can install anything + ## Don't install on MIC or WEB servers, only APP servers. + if ( -not (Test-IsDeveloperMachine) -and -not (Test-IsAppServer)) { + Write-Warning "$loglead : Can ONLY install providers on APP machines or DEVELOPER machines" + return + } + if ( -not (Test-IsAdmin)) { + throw "You are not running as administrator. Can not continue." + } + + # See if there is an override + if ($NoSymlink) { + Write-Host "$loglead : Override provided! Using Legacy non-Symlink Installer Strategy" + $usingSymlinkStrategy = $false + } else { + $usingSymlinkStrategy = Test-InstallerUseSymlinkStrategy + + if ($usingSymlinkStrategy) { + Write-Host "$loglead : Using Symlink Installer Strategy." + } else { + Write-Host "$loglead : Using Legacy non-Symlink Installer Strategy" + } + } + + $copyScriptBlock = { + param($sbSourcePath, $sbTargetPath, $sbUseSymlinkStrategy, $sbLoglead) + + $sbloglead = "$sbLoglead (copyScriptBlock)" + $sbTargetFilename = Split-Path -Path $sbSourcePath -Leaf + $sbTargetDestination = Join-Path -Path $sbTargetPath -ChildPath $sbTargetFilename + #If sbTargetPath exists, delete it + #NOTE - the Symlink codepath SPECIFICALLY DOES NOT DELETE + if (Test-Path -Path $sbTargetDestination) { + Write-Host "$sbloglead : Removing non-symlinked file at destination $sbTargetDestination" + # This is to remove legacy files when upgrading to new Symlink Strategy. + Remove-FileSystemItem -Path $sbTargetDestination -Force -SkipSymlinks + } + + if ($sbUseSymlinkStrategy) { + Write-Host "$sbloglead : Symlinking file $sbSourcePath into Destination $sbTargetPath" + New-Symlink -Path $sbSourcePath -Destination $sbTargetDestination + } else { + Write-Host "$sbloglead : Copying file $sbSourcePath into Destination $sbTargetPath" + Copy-Item -Path $sbSourcePath -Destination $sbTargetPath -Force | Out-Null + } + } + + if ( -not (Test-Path -Path $SourcePath)) { + throw "Could not find the source path specified at $SourcePath" + } + + $libSourcePath = Join-Path -Path $SourcePath -ChildPath "lib" + + ## We aren't in a valid Alkami choco package if this is the case. + if ( -not (Test-Path -Path $libSourcePath)) { + throw "Could not find the lib folder path specified at $libSourcePath" + } + + $libSourceFSOFolder = (Get-ChildItem $libSourcePath -Directory) | Sort-Object -Descending | Select-Object -First 1 + # SRE-16977 - following line should be this + # $libSourceFolderPath = $libSourceFSOFolder.FullName + # However, developers implemented packages that UNINTENTIONALLY took advantage of this bug + # We must reintroduce this bug in order to not break the deployment of malformed packages + # This allows for the preceding Get-ChildItem to return $null and the following line to return + # the path it was trying to Get-ChildItem from without problem, instead of $null + $libSourceFolderPath = Join-Path -Path $libSourcePath -ChildPath $libSourceFSOFolder.Name + + #TODO: Get these only once for the $childFiles array below + if ((Get-ChildItem -Path $libSourceFolderPath -Filter "*.dll").Count -eq 0 -and (Get-ChildItem -Path $libSourceFolderPath -Filter "*.exe").Count -eq 0) { + throw "Could not find any dll or exe files under $libSourceFolderPath" + } + + if ( -not (Get-ChildItem $libSourceFolderPath -Filter "$ProviderAssemblyInfo.dll")) { + $message = "The folder $libSourceFolderPath does not have a $ProviderAssemblyInfo dll file. This is in violation of package structuring. The Dev team must fix this so the $ProviderAssemblyInfo package id matches the principal dll of the package." + Write-Error $message + throw $message + } + + $folderTargets = @() + $orbPath = Get-OrbPath + + ## If the following services are in any status other than Stopped, we want to stop, then restart them when on a NON-PROD machine. + ## Production servers are always deployed to when out-of-pool, so the services are already, in theory, stopped. + $isRadiumRunning = $false + $radiumServiceName = @(Get-ServiceNamesByFragment -Name "Radium")[0] + if ($ProviderTargets -contains "Radium") { + if ($radiumServiceName -ne "Not installed") { + $isRadiumRunning = (Get-Service -Name $radiumServiceName).Status -ne "Stopped" + if (Test-Path -Path (Join-Path -Path (Get-OrbPath) -ChildPath 'Radium')) { + $folderTargets += Join-Path -Path $orbPath -ChildPath "Radium" + } + } + } + + $isNagRunning = $false + $nagServiceName = @(Get-ServiceNamesByFragment -Name "Alkami Nag")[0] + if ($ProviderTargets -contains "Nag") { + if ($nagServiceName -ne "Not installed") { + $isNagRunning = (Get-Service -Name $nagServiceName).Status -ne "Stopped" + if (Test-Path -Path (Join-Path -Path (Get-OrbPath) -ChildPath 'Nag')) { + $folderTargets += Join-Path -Path $orbPath -ChildPath "Nag" + } + } + } + + $isBankRunning = $false + if ($ProviderTargets -contains "BankService") { + $isBankRunning = Test-IISAppPoolByName -Name "BankService" + $bankServicePath = Join-Path -Path $orbPath -ChildPath "BankService" + $bankServiceBinPath = Join-Path -Path $bankServicePath -ChildPath "bin" + if (Test-Path -Path $bankServiceBinPath) { + $folderTargets += $bankServiceBinPath + } + } + + $isSchedulerRunning = $false + if ($ProviderTargets -contains "SchedulerService") { + $isSchedulerRunning = Test-IISAppPoolByName -Name "SchedulerService" + $SchedulerServicePath = Join-Path -Path $orbPath -ChildPath "SchedulerService" + $SchedulerServiceBinPath = Join-Path -Path $SchedulerServicePath -ChildPath "bin" + if (Test-Path -Path $SchedulerServiceBinPath) { + $folderTargets += $SchedulerServiceBinPath + } + } + + $isCoreRunning = $false + if ($ProviderTargets -contains 'CoreService') { + $isCoreRunning = Test-IISAppPoolByName -Name "CoreService" + ## CoreService is included because occasionally it may use other providers. + $coreServicePath = Join-Path -Path $orbPath -ChildPath "CoreService" + $coreServiceBinPath = Join-Path -Path $coreServicePath -ChildPath "bin" + if (Test-Path -Path $coreServiceBinPath) { + $folderTargets += $coreServiceBinPath + } + } + + $isNagConfigRunning = $false + if ($ProviderTargets -contains 'NagConfigurationService') { + $isNagConfigRunning = Test-IISAppPoolByName -Name "NagConfigurationService" + $nagConfigurationServicePath = Join-Path -Path $orbPath -ChildPath "NagConfigurationService" + $nagConfigurationServiceBinPath = Join-Path -Path $nagConfigurationServicePath -ChildPath "bin" + if (Test-Path -Path $nagConfigurationServiceBinPath) { + $folderTargets += $nagConfigurationServiceBinPath + } + } + + $isNotifyRunning = $false + if ($ProviderTargets -contains 'NotificationService') { + $isNotifyRunning = Test-IISAppPoolByName -Name "NotificationService" + $notificationServicePath = Join-Path -Path $orbPath -ChildPath "NotificationService" + $notificationServiceBinPath = Join-Path -Path $notificationServicePath -ChildPath "bin" + if (Test-Path -Path $notificationServiceBinPath) { + $folderTargets += $notificationServiceBinPath + } + } + + $isSecMgmtRunning = $false + if ($ProviderTargets -contains 'SecurityManagementService') { + $isSecMgmtRunning = Test-IISAppPoolByName -Name "SecurityManagementService" + $securityManagementServicePath = Join-Path -Path $orbPath -ChildPath "SecurityManagementService" + $securityManagementServiceBinPath = Join-Path -Path $securityManagementServicePath -ChildPath "bin" + if (Test-Path -Path $securityManagementServiceBinPath) { + $folderTargets += $securityManagementServiceBinPath + } + } + + if ($isRadiumRunning) { + Stop-AlkamiService -ServiceName $radiumServiceName + } + + if ($isNagRunning) { + Stop-AlkamiService -ServiceName $nagServiceName + } + + if ($isBankRunning) { + Stop-IISAppPoolByName -Name "BankService" + Remove-DotNetTemporaryFiles -Name "Bank" + } + + if ($isSchedulerRunning) { + Stop-IISAppPoolByName -Name "SchedulerService" + Remove-DotNetTemporaryFiles -Name "SchedulerService" + } + + if ($isCoreRunning) { + Stop-IISAppPoolByName -Name "CoreService" + Remove-DotNetTemporaryFiles -Name "CoreService" + } + + if ($isNagConfigRunning) { + Stop-IISAppPoolByName -Name "NagConfigurationService" + Remove-DotNetTemporaryFiles -Name "NagConfigurationService" + } + + if ($isNotifyRunning) { + Stop-IISAppPoolByName -Name "NotificationService" + Remove-DotNetTemporaryFiles -Name "NotificationService" + } + + if ($isSecMgmtRunning) { + Stop-IISAppPoolByName -Name "SecurityManagementService" + Remove-DotNetTemporaryFiles -Name "securityManagement" + } + + if ($isBankRunning -or $isCoreRunning -or $isNagConfigRunning -or $isNotifyRunning -or $isSecMgmtRunning -or $isRadiumRunning -or $isNagRunning -or $isSchedulerRunning) { + Start-Sleep -Seconds 30 + } + + $childFiles = @() + $childFiles += @(Get-ChildItem -Path $libSourceFolderPath -Filter "*.dll") + $childFiles += @(Get-ChildItem -Path $libSourceFolderPath -Filter "*.exe") + foreach ($childFile in $childFiles) { + $pdbPath = $file.FullName -replace $file.Extension, ".pdb" + if (Test-Path -Path $pdbPath) { + $childFiles += $pdbPath + } + } + + Write-Host "$loglead : Files to copy or link: " + Write-Host ("$loglead : `n`t{0}" -f ($childFiles.FullName -join "`n`t")) + + ## THIS COMMENT APPLIES TO THE SCRIPTBLOCK BEING CALLED BY Invoke-CommandWithRetry + ## As we've already checked the files against the shared folder in ORB, let's now check each individual service + ## bin folder for the pared-down list of files from the package. + ## If the file in the bin folder already exists and matches/wins, we don't need to do anything. + ## The anticipation is that the list of files to be checked here is approximately 5-8, or less. + ## That's 40 potential file comparisons across five services, so it should be relatively fast. + foreach($targetFolder in $folderTargets) { + foreach($filePath in $childFiles.FullName) { + Write-Host "$loglead : Copying file $filePath into Destination $targetFolder with -Force" + Write-Information '.' + + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($filePath, $targetFolder, $usingSymlinkStrategy, $loglead) -ScriptBlock $copyScriptBlock + } + } + + if ($isBankRunning) { + Start-IISAppPoolByName -Name "BankService" + } + if ($isSchedulerRunning) { + Start-IISAppPoolByName -Name "SchedulerService" + } + if ($isCoreRunning) { + Start-IISAppPoolByName -Name "CoreService" + } + if ($isNagConfigRunning) { + Start-IISAppPoolByName -Name "NagConfigurationService" + } + if ($isNotifyRunning) { + Start-IISAppPoolByName -Name "NotificationService" + } + if ($isSecMgmtRunning) { + Start-IISAppPoolByName -Name "SecurityManagementService" + } + if ($isRadiumRunning) { + Start-Service -Name $radiumServiceName + } + if ($isNagRunning) { + Start-Service -Name $nagServiceName + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.ps1 new file mode 100644 index 0000000..ce1ae54 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.ps1 @@ -0,0 +1,145 @@ +Function Install-WebApplication { +<# +.SYNOPSIS + Install a Web Application to the appropriate place + +.DESCRIPTION + Install a Web Application to the appropriate place. + Will ensure appropriate app pool exists + +.PARAMETER WebAppName + [string] The name of the web application. + +.PARAMETER SourcePath + [string] The folder that contains the files. Typically a chocolatey path. + +.PARAMETER NeedsShared + [switch] Indicates that the legacy utility requires the use of the ORB shared folder. This is a typical configuration for the truly legacy Alkami ORB utility projects. + +.PARAMETER IsClient + [switch] Is this package installed to client? + +.PARAMETER IsAdmin + [switch] Is this package installed to admin? + +.PARAMETER IsLegacy + [switch] Is this package installed to the legacy site? (typically Default Web Site) + +.PARAMETER NoManagedCode + [switch] Is this .net core, or Managed code? + +.PARAMETER AppPoolName + [switch] App pool name. Required for .net core apps. That is, if -NoManagedCode is included. + +.INPUTS + WebAppName and SourcePath are required. + Requires one of IsClient or IsAdmin or IsLegacy + +.OUTPUTS + Various diagnostic information about the install process + +.EXAMPLE + Install-WebApplication -WebAppName BankService -SourcePath C:\Orb\BankService -IsLegacy + #> + [CmdletBinding(DefaultParameterSetName='IsClient')] + Param( + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=0)] + [string]$WebAppName, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=1)] + [Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=1)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=1)] + [string]$SourcePath, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$false)] + [Parameter(ParameterSetName='IsClient',Mandatory=$false)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$false)] + [switch]$NeedsShared, + [Parameter(ParameterSetName='IsClient',Mandatory=$true)] + [switch]$IsClient, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true)] + [switch]$IsAdmin, + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true)] + [switch]$IsLegacy, + [switch]$NoManagedCode, + [string]$AppPoolName + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + if($NoManagedCode){ + if ([string]::IsNullOrWhiteSpace($AppPoolName)) { + Write-Error "$logLead : When installing a .net core app, an App Pool Name is required." + } + } + + Write-Host "$loglead : $WebAppName being installed by $($env:username) on $($env:computername) at $(Get-Date)" + + ## Assumption: We never copy files for WebApplications being installed because the files already exist on disk + ## Legacy: copied into C:\orb along with rest of deploy. Ex: BankService + ## Not-Legacy: decompressed into choco-lib folder and linked to IIS from there: ex: CUFX + ## Not-Legacy: may require shared folder link, see next block for $NeedsShared + if (!(Test-Path $SourcePath)) { + throw "Could not find the source path specified at $SourcePath"; + } + + if ($NeedsShared -and (Test-PathIsInApprovedPackageLocation $SourcePath)) { + ## This is in a choco or similar location. + ## We should take the parent of our path and ensure that a "Shared" symlink exists at that point + ## The purpose of this is for Alkami.Ioc resolver to find the parent path in the lookup + ## This introduces a hard-limit that no package can be called Shared (unless it's the Legacy ORB "Shared" folder from the build process) + $SharedTargetPath = (Join-Path (Split-Path $SourcePath -Parent) "Shared") + + if (!(Test-Path $SharedTargetPath)) { + ## Create the symlink here + $sharedOrbPath = Get-OrbSharedPath + New-Symlink -path $sharedOrbPath -Destination $SharedTargetPath + } + } + + ## If this is a legacy app, and we are on a web-server, we can't install it + ## If this is a client or admin app, and we are on not on a web-server, we can't install it. + ## If this is a developer machine we can install it + if (Test-IsDeveloperMachine) { + Write-Host "$loglead : Running as developer, should allow anything to occur" + } elseif ((Test-IsWebServer) -and $IsLegacy) { + Write-Warning "$loglead : Can not install app-tier web applications on the web tier" + return + } elseif (!(Test-IsWebServer) -and ($IsClient -or $IsAdmin)) { + Write-Warning "$loglead : Can not install web-tier web applications on the app tier" + return + } + + if($NoManagedCode){ + if ($IsLegacy) { + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $SourcePath -IsLegacy -NoManagedCode -AppPoolName $AppPoolName + } elseif ($IsClient) { + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $SourcePath -IsClient -NoManagedCode -AppPoolName $AppPoolName + } elseif ($IsAdmin) { + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $SourcePath -IsAdmin -NoManagedCode -AppPoolName $AppPoolName + } + } + else{ + if ($IsLegacy) { + $legacyTargetPath = Join-Path -Path (Get-OrbPath) -ChildPath $WebAppName + $legacySourcePath = Join-Path $SourcePath -ChildPath "Content\App\*" + if (-not (Test-Path -Path $legacySourcePath)) { + throw "$logLead : Could not find the expected path for the componentized WebApplication at [$legacySourcePath]. Did this package get built according to the Confluence documentation?" + } + if (-not (Test-Path -Path $legacyTargetPath)) { + New-Item -ItemType Directory -Path $legacyTargetPath -Force + } + Copy-Item -Path $legacySourcePath -Destination $legacyTargetPath -Recurse -Force + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $legacyTargetPath -IsLegacy + } elseif ($IsClient) { + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $SourcePath -IsClient + } elseif ($IsAdmin) { + Install-AlkamiWebApplication -WebAppName $WebAppName -SourcePath $SourcePath -IsAdmin + } + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.tests.ps1 new file mode 100644 index 0000000..0ee7724 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-WebApplication.tests.ps1 @@ -0,0 +1,142 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Install-WebApplication" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Test-IsAdmin -MockWith { return $true } + + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-OrbSharedPath -MockWith { return "TestDrive:\Orb\Shared" } + Mock -ModuleName $moduleForMock -CommandName Get-OrbPath -MockWith { return "TestDrive:\Orb" } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Test-PathIsInApprovedPackageLocation {return $true} + + $fakeAppName = 'FakeApp' + $fakePath = (Join-Path (Get-OrbPath) 'FakePath') + $fakeAppPoolName = "FakeAppPool" + + Context "When Source Path Doesn't Exist"{ + It "Throws"{ + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $false } + + { Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient -AppPoolName $fakeAppPoolName } | Should -Throw + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + + Context "When Is Not Running As Admin"{ + It "Throws"{ + Mock -ModuleName $moduleForMock -CommandName Test-IsAdmin -MockWith { return $false } + + { Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient -AppPoolName $fakeAppPoolName } | Should -Throw + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-IsAdmin -MockWith { return $true } + + Context "When -NeedsShared Switch is Supplied And SharedTargetPath Doesn't Exist"{ + It "Calls New-Symlink"{ + Mock -ModuleName $moduleForMock -CommandName New-Symlink -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith {return $false} -ParameterFilter { $Path -eq "TestDrive:\Orb\Shared" } + + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient -AppPoolName $fakeAppPoolName -NeedsShared + + Assert-MockCalled -CommandName New-Symlink -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + } + + Context "When Testing If Is A Valid Install"{ + It "Writes To Console If It's a Dev Machine" { + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith {return $true} + + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -IsClient -AppPoolName $fakeAppPoolName + + Assert-MockCalled -CommandName Write-Host -Times 1 -Scope It -Exactly -ModuleName $moduleForMock -ParameterFilter { $Object -like "*Running as developer*" } + } + + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith {return $false} + + It "Writes a Warning And Returns If It's A Package For The Legacy Site, And On A Webserver"{ + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith {return $true} + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsLegacy + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -Exactly -ModuleName $moduleForMock -ParameterFilter {$Message -like "*Can not install app-tier web applications on the web tier*"} + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 0 -Scope It -Exactly -ModuleName $moduleForMock + } + + It "Writes a Warning And Returns If It's A Package For The Client Site, And Not On A Webserver"{ + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer -MockWith {return $false} + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsClient + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -Exactly -ModuleName $moduleForMock -ParameterFilter {$Message -like "*Can not install web-tier web applications on the app tier*"} + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 0 -Scope It -Exactly -ModuleName $moduleForMock + } + + It "Writes a Warning And Returns If It's A Package For The Admin Site, And Not On A Webserver"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsAdmin + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope It -Exactly -ModuleName $moduleForMock -ParameterFilter {$Message -like "*Can not install web-tier web applications on the app tier*"} + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 0 -Scope It -Exactly -ModuleName $moduleForMock + } + } + + Context "When Installing NoManagedCode"{ + It "Calls Install-AlkamiWebApplication When -IsLegacy Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsLegacy -NoManagedCode + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + It "Calls Install-AlkamiWebApplication When -IsClient Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsClient -NoManagedCode + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + It "Calls Install-AlkamiWebApplication When IsAdmin Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsAdmin -NoManagedCode + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + } + + Context "When Installing Managed Code"{ + It "Throws When LegacySourcePath Doesn't Exist and -IsLegacy Switch Is Provided"{ + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith {return $false} -ParameterFilter { $Path -eq "TestDrive:\Orb\FakePath\Content\App\*" } + { Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsLegacy } | Should -Throw + } + It "Creates A New Directory When legacyTargetPath Doesn't Exist and -IsLegacy Switch Is Provided"{ + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith {return $true} -ParameterFilter { $Path -eq "TestDrive:\Orb\FakePath\Content\App\*" } + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith {return $false} -ParameterFilter { $Path -eq "TestDrive:\Orb\FakeApp" } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Copy-Item -MockWith {} + + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsLegacy + + Assert-MockCalled -CommandName New-Item -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + It "Calls Install-AlkamiWebApplication When -IsLegacy Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsLegacy + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + It "Calls Install-AlkamiWebApplication When -IsClient Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsClient + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + It "Calls Install-AlkamiWebApplication When IsAdmin Switch Is Provided"{ + Install-WebApplication -WebAppName $fakeAppName -SourcePath $fakePath -AppPoolName $fakeAppPoolName -IsAdmin + + Assert-MockCalled -CommandName Install-AlkamiWebApplication -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-WebExtension.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-WebExtension.ps1 new file mode 100644 index 0000000..cf5b3a6 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-WebExtension.ps1 @@ -0,0 +1,186 @@ +Function Install-WebExtension { +<# +.SYNOPSIS + Install a WebExtension to the appropriate place (generally C:\orb\) + +.DESCRIPTION + Install a WebExtension to the appropriate place. (generally C:\orb\) + Will make certain assumptions based on best practices, such as locations to install to based on flags. + +.PARAMETER ExtensionName + [string] The name of the extension. Will be used to build a dynamic path. Typically the package id. + +.PARAMETER SourcePath + [string] The folder that contains the files. Typically a chocolatey path. + +.PARAMETER IsAdmin + [switch] Is this package installed to admin? + +.PARAMETER RemoveLogs + [switch] Remove logs if the service was running? + +.PARAMETER NoSymlink + [switch] Override the machine settings for handling Symlinks? + +.INPUTS + ExtensionName and SourcePath are required. + +.OUTPUTS + Various diagnostic information about the install process + +.EXAMPLE + Install-WebExtension -ExtensionName Alkami.Client.WebExtension.Example -SourcePath C:\ProgramData\chocolatey\lib\Alkami.Client.WebExtension.Example + +Various diagnostic information about the install process. + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$ExtensionName, + [Parameter(Mandatory=$true, Position=1)] + [string]$SourcePath, + [switch]$isAdmin, + [switch]$RemoveLogs, + [Parameter(Mandatory = $false)] + [switch]$NoSymlink + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsDeveloperMachine) -and !(Test-IsWebServer)) { + Write-Warning "$loglead : Can not install WebExtensions on the app tier" + return + } + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + $usingSymlinkStrategy = $false + + # See if there is an override + if($NoSymlink) { + Write-Host "$loglead : Override provided! Skipping Symlink strategy." + } else { + $usingSymlinkStrategy = Test-InstallerUseSymlinkStrategy + + if($usingSymlinkStrategy) { + Write-Host "$loglead : Using Symlink Installer Strategy." + } else { + Write-Host "$loglead : Using Legacy non-Symlink Installer Strategy" + } + } + + Write-Host "$loglead : $ExtensionName being installed by $($env:username) on $($env:computername) at $(Get-Date)" + + # Build the Source Path + if (!(Test-Path $SourcePath)) { + throw "Could not find the source path specified at $SourcePath" + } + + # Append lib to the chocolatey directory + # ex: c:\ProgramData\chocolatey\lib\Alkami.Sample.Service\lib + $libSourcePath = (Join-Path $SourcePath 'lib') + + if (!(Test-Path $libSourcePath)) { + throw "Could not find the lib folder path specified at $libSourcePath" + } + + ## Write warning that more than one lib folder was detected + # Go into the first subfolder. + # ex: c:\ProgramData\chocolatey\lib\Alkami.Sample.Service\lib\net472 + $libSourceFolderName = (Get-ChildItem $libSourcePath -Directory) | Sort-Object -Descending | Select-Object -First 1 + + $libSourceFolder = (Join-Path $libSourcePath $libSourceFolderName) + + if ((Get-ChildItem (Join-Path $libSourceFolder "*.dll")).Count -eq 0) { + throw "Could not find any dll files under $libSourceFolder" + } + + Write-Host "$loglead : Looks like we are ready to install $ExtensionName" + + $appName = 'WebClient' + + if ($isAdmin) { + $appName = 'WebClientAdmin' + } + + # Build the target path + # ex: c:\orb\WebClient\Modules + $moduleRootPath = Join-Path (Join-Path (Get-OrbPath) $appName) 'Modules' + # ex: c:\orb\WebClient\Modules\Alkami.Sample.Service + $orbExtensionPath = (Join-Path -Path $moduleRootPath -ChildPath $ExtensionName) + + $isRunning = (Test-IISAppPoolByName $appName) + if ($isRunning) { + Write-Host "$loglead : Stopping $appName" + Stop-WebAppPool $appName + Do + { + Start-Sleep -Milliseconds 100 + } + Until ((Get-WebAppPoolState -Name $appName).Value -eq "Stopped" ) + } + + # Check if we're Removing logs and it was running (because there should be no new logs if it isn't running) + if ($RemoveLogs -and $isRunning) { + $logfiles = Get-LogPathsForOrbApplication $appName + + # Delete each log file. + foreach($logPath in $logfiles) { + if (Test-Path $logPath) { + Remove-FileSystemItem $logPath -ErrorAction Stop + } + } + } + + ## Clear .NET Temp files associated with the site + $tempFilePath = (Get-SiteTempDirectoryPath $appName) + if ($isRunning -and !([string]::IsNullOrWhiteSpace($tempFilePath)) -and (Test-Path $tempFilePath)) { + Remove-FileSystemItem $tempFilePath -Recurse -ErrorAction Stop + } + + # Always delete the target folder as we'll recreate it or symlink it below + if (Test-Path $orbExtensionPath) { + ## Update OrbCore manifest if it exists to say that I've removed these files from the folder + Remove-FileSystemItem $orbExtensionPath -Recurse -ErrorAction Stop + } + + ## This is because we are changing the way we path our extensions + ## Originally people got to choose their own folder name, ex: Signalr + ## We are changing the pattern to Alkami.Modules.SignalR for the folder name + ## Therefore, if the old folder exists at the last part of the split, then we remove that folder too + ## That way there's only one version of the module, assuming reasonable assumptions from developer discussions + $badOldFolderName = ($ExtensionName -split '\.')[-1] + $badOldFolderPath = (Join-Path (Join-Path (Join-Path (Get-OrbPath) $appName) 'Modules') $badOldFolderName) + + if (Test-Path $badOldFolderPath) { + ## Update OrbCore manifest if it exists to say that I've removed these files from the folder + Write-Host "I am removing all files from $badOldFolderPath because it is an old path." + Remove-FileSystemItem $badOldFolderPath -Recurse -ErrorAction Stop + } + + if($usingSymlinkStrategy) { + # Setup Symlinks + Write-Host "$loglead : In the block to do Symlinking" + Write-Host "$loglead : Symlinking files from $libSourceFolder into Destination $moduleRootPath" + + New-Symlink -path $libSourceFolder -Destination $moduleRootPath -TargetName $ExtensionName + } else { + Write-Host "$loglead : In the block to do Copying." + ## It should never exist because we deleted it but let's make sure it doesn't ... + if (!(Test-Path $orbExtensionPath)) { + (New-Item $orbExtensionPath -ItemType Directory) | Out-Null + } + + Write-Host "$loglead : Copying contents of $libSourceFolder into Destination $orbExtensionPath" + (Copy-Item -Path (Join-Path $libSourceFolder '*') -Destination $orbExtensionPath -Recurse -Force) | Out-Null + } + + if ($isRunning) { + Write-Host "$loglead : Starting $appName" + Start-WebAppPool $appName + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Install-Widget.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Install-Widget.ps1 new file mode 100644 index 0000000..85b228c --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Install-Widget.ps1 @@ -0,0 +1,299 @@ +Function Install-Widget { +<# +.SYNOPSIS + Installs a widget either on a server or a developer machine from a choco location, stops and starts IIS if it was running before. + +.EXAMPLE + Install-Widget -WidgetName Authentication -SourcePath c:\programdata\chocolatey\lib\alkami.apps.authentication + +.PARAMETER WidgetName + The name of the widget to install + +.PARAMETER SourcePath + The path to the widget to be installed, usually a chocolatey lib folder + +.PARAMETER IsAdmin + Indicate that the base web application name to look in is WebClientAdmin, otherwise is WebClient + +.PARAMETER RemoveLogs + Indicating that the ORB logs should be purged + +.PARAMETER NoSymlink + Force installation using File Copy operations, omitting a call to Test-InstallerUseSymlinkStrategy, even on hosts with the ENVIRONMENT VARIABLE 'Alkami.Installer.UseSymlink' set + +.LINK + Test-InstallerUseSymlinkStrategy + +.OUTPUTS + This function will only Write-Information, but also show the files that will be tentatively copied over + Install-Widget output may be verbose from the underlying calls being made. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$WidgetName, + + [Parameter(Mandatory=$true, Position=1)] + [string]$SourcePath, + + [Parameter(Mandatory = $false)] + [switch]$IsAdmin, + + [Parameter(Mandatory = $false)] + [switch]$RemoveLogs, + + [Parameter(Mandatory = $false)] + [switch]$NoSymlink + ) + + $loglead = Get-LogLeadName + Write-Host "$loglead : $WidgetName being installed by $($env:USERNAME) on $($env:COMPUTERNAME) at $(Get-Date)" + + if ( -not (Test-IsDeveloperMachine) -and -not (Test-IsWebServer)) { + Write-Warning "$loglead : Cannot install widgets on the APP tier" + return + } + if ( -not (Test-IsAdmin)) { + throw "You are NOT running as administrator. Cannot continue." + } + + if ($NoSymlink) { + Write-Host "$loglead : Override provided! Using Legacy non-Symlink Installer Strategy" + $usingSymlinkStrategy = $false + } else { + $usingSymlinkStrategy = Test-InstallerUseSymlinkStrategy + + if ($usingSymlinkStrategy) { + Write-Host "$loglead : Using Symlink Installer Strategy." + } else { + Write-Host "$loglead : Using Legacy non-Symlink Installer Strategy" + } + } + + if ($usingSymlinkStrategy) { + $FILE_OPERATION_STRING = "SYMLINKing" + } else { + $FILE_OPERATION_STRING = "COPYing" + } + + if ( -not (Test-Path -Path $SourcePath)) { + throw "$loglead : Could not find the source path specified at $SourcePath" + } + + $libSourcePath = Join-Path -Path $SourcePath -ChildPath 'lib' + + if ( -not (Test-Path -Path $libSourcePath)) { + throw "$loglead : Could not find the lib folder path specified at $libSourcePath" + } + + $libSourceFSOFolder = Get-ChildItem -Path $libSourcePath -Directory ` + | Sort-Object -Descending ` + | Where-Object {(Get-ChildItem -Path $_.FullName -filter "*.dll").Count -gt 0 } ` + | Select-Object -First 1 + # This was labeled as part of, and committed against, SRE-16977 but that's the wrong ticket number. + + # The following line should be this: + # $libSourceFolderPath = $libSourceFSOFolder.FullName + # However, developers implemented packages that UNINTENTIONALLY took advantage of a bug where the .net version subfolders didn't exist. + # We must handle this bug in order to not break the deployment of malformed packages + # This allows for the preceding Get-ChildItem to return $null and the following line to return + # the path it was trying to Get-ChildItem from without problem, instead of $null + + # SRE-18955 - Added the Where-Object clause above to handle folders existing with no dlls due to poor choco cleanup. + # libSourceFSOFolder may be null (and that's acceptable). So .name will be null + # and $libSourceFolderPath will correctly be $libSourcePath. + + $libSourceFolderPath = Join-Path -Path $libSourcePath -ChildPath $libSourceFSOFolder.Name + Write-Host "$loglead : Looking for dll files in $libSourceFolderPath" + + if ((Get-ChildItem -Path $libSourceFolderPath -Filter *.dll).Count -eq 0) { + throw "$loglead : Could not find any dll files under $libSourceFolderPath" + } + + ## Second use-case will be for installing from a project folder and getting this from the bin path of the built solution + ## That above case is to-do for cbrand - 2018-08-16 + $contentTopPath = Join-Path -Path $SourcePath -ChildPath "Content" + $contentSourcePath = Join-Path -Path $contentTopPath -ChildPath "Areas" + + $contentSourcePathFound = $true + + if ( -not (Test-Path -Path $contentSourcePath)) { + ## Don't throw right here because this could be an API widget + Write-Warning "$loglead : Is this an API style widget? Could not find the content path specified at $contentSourcePath" + $contentSourcePathFound = $false + } + if ($contentSourcePathFound -and (Get-ChildItem $contentSourcePath -Directory).Count -eq 0) { + ## Don't throw right here because this could be an API widget + Write-Warning "$loglead : Is this an API style widget? Could not find any content folders under $contentSourcePath" + $contentSourcePathFound = $false + } + + $contentSourceFolderPath = "" + + if ($contentSourcePathFound) { + $preferredContentSourceFolderPath = Join-Path -Path $contentSourcePath -ChildPath "App" + $contentSourceFolderPath = $preferredContentSourceFolderPath + + if ( -not (Test-Path -Path $contentSourceFolderPath)) { + Write-Host "$loglead : Not able to find the content folder $contentSourceFolderPath looking for another" + $contentSourceFSOFolder = (Get-ChildItem $contentSourcePath -Directory) | Sort-Object -Descending | Select-Object -First 1 + # SRE-16977 - following line should be this + # $contentSourceFolderPath = $contentSourceFSOFolder.FullName + # However, developers implemented packages that UNINTENTIONALLY took advantage of this bug + # We must reintroduce this bug in order to not break the deployment of malformed packages + # This allows for the preceding Get-ChildItem to return $null and the following line to return + # the path it was trying to Get-ChildItem from without problem, instead of $null + $contentSourceFolderPath = Join-Path -Path $contentSourcePath -ChildPath $contentSourceFSOFolder.Name + } + + Write-Host "$loglead : Found content folder $contentSourceFolderPath" + } + + Write-Host "$loglead : Ready to install $WidgetName" + Write-Host "$loglead : $FILE_OPERATION_STRING DLL/LIB files from $libSourceFolderPath" + + if ($contentSourcePathFound) { + Write-Host "$loglead : $FILE_OPERATION_STRING CONTENT files from $contentSourceFolderPath" + } + + $appName = "WebClient" + + if ($IsAdmin) { + $appName = "WebClientAdmin" + } + + ## Get the IIS sites by path then get the distinct application pool names for all sites bound to this path. + $ORB_PATH = Get-OrbPath + $orbBaseWebclientPath = Join-Path -Path $ORB_PATH -ChildPath $appName + Write-Host "$loglead : Orb Base webclient path $orbBaseWebclientPath" + + $iisApplicationPools = @() + $iisSites = Get-IISSitesByPath -Path $orbBaseWebclientPath + foreach ($site in $iisSites) { + if ($site.ApplicationPool -notin $iisApplicationPools) { + $iisApplicationPools += $site.ApplicationPool + } + } + + $appPathTemp = Join-Path -Path $ORB_PATH -ChildPath $appName + $orbAreaPath = Join-Path -Path $appPathTemp -ChildPath "Areas" + $orbAreaWidgetContentPath = Join-Path $orbAreaPath $WidgetName + $orbAreaBinPath = Join-Path $orbAreaWidgetContentPath "bin" + + # In the case of using the symlink installer strategy, we just always goto to the app root bin + # In the non-symlink case, we would send to C:\Orb\WebClientAdmin\bin or C:\Orb\WebClient\Areas\\bin + if ($IsAdmin -or $usingSymlinkStrategy) { + $orbAreaBinPath = Join-Path -Path $appPathTemp -ChildPath "bin" + } + + ## Stop any/all $iisApplicationPools that are running. + $isRunning = $false + foreach ($appPool in $iisApplicationPools) { + Write-Host "$loglead : Test-IISAppPoolByName -Name $appPool" + $isSiteRunning = Test-IISAppPoolByName -Name $appPool + if ($isSiteRunning) { + $isRunning = $true + } + + if($isSiteRunning) { + Write-Host "$loglead : Stopping $appPool" + (Stop-WebAppPool -Name $appPool) | Out-Null + Do { + Start-Sleep -Milliseconds 100 + } Until ((Get-WebAppPoolState -Name $appPool).Value -eq "Stopped" ) + } + } + + if ($RemoveLogs -and $isRunning) { + $logfiles = Get-LogPathsForOrbApplication -AppName $appName + foreach($logPath in $logfiles) { + if (Test-Path -Path $logPath) { + Write-Host "$loglead : Removing $logPath" + (Remove-FileSystemItem -Path $logPath -ErrorAction Stop) | Out-Null + } + } + } + + # Remove the folder to the widget areas in Orb before starting. + $symlinkAreaContentFolderAlreadyCorrect = $false + if ($usingSymlinkStrategy) { + $isOrbAreaWidgetPathSymlink = Test-IsSymlink -Path $orbAreaWidgetContentPath + Write-Host "$loglead : Is the existing ORB Widget Area path already a Symlink? $isOrbAreaWidgetPathSymlink" + if ($isOrbAreaWidgetPathSymlink) { + $symlinkAreaContentFolderAlreadyCorrect = Test-PathMatch -FirstPath $orbAreaWidgetContentPath -SecondPath $contentSourceFolderPath + } + } + + if ((Test-Path -Path $orbAreaWidgetContentPath) -and -not $symlinkAreaContentFolderAlreadyCorrect) { + Write-Host "$loglead : Removing item $orbAreaWidgetContentPath" + (Remove-FileSystemItem -Path $orbAreaWidgetContentPath -Recurse -ErrorAction Stop) | Out-Null + } + + if ($usingSymlinkStrategy) { + Write-Host "$loglead : $FILE_OPERATION_STRING folder $contentSourceFolderPath into Destination $orbAreaWidgetContentPath" + if ($contentSourcePathFound) { + # Link the \App\ folder to \ + New-Symlink -Path $contentSourceFolderPath -Destination $orbAreaPath -Name $WidgetName + } + Write-Host "$loglead : Get all of the dll, pdb and xml files in $libSourceFolderPath to be symlinked directly to $orbAreaBinPath" + $filesInlibSourceFolderPath = Get-ChildItem -Path $libSourceFolderPath -Recurse + $fileTypesToLink = @(".dll",".pdb",".xml") + + foreach ($file in $filesInlibSourceFolderPath) { + if ($file.Extension -in $fileTypesToLink) { + $libFilePath = Join-Path -Path $libSourceFolderPath -ChildPath $file + $orbAreaBinFilePath = Join-Path -Path $orbAreaBinPath -ChildPath $file + + if ( -not (Test-IsSymlink -Path $orbAreaBinFilePath)) { + New-Symlink -Path $libFilePath -Destination $orbAreaBinPath + } + } + } + } else { + ## Legacy filecopy codepath + foreach ($iisSite in $iisSites) { + $tempFilePath = Get-SiteTempDirectoryPath -siteOrAppName $($iisSite.Name) ##$appName + if ($isRunning -and -not ([string]::IsNullOrWhiteSpace($tempFilePath)) -and (Test-Path -Path $tempFilePath)) { + Write-Host "$loglead : Removing item $tempFilePath" + (Remove-FileSystemItem -Path $tempFilePath -Recurse -ErrorAction Stop) | Out-Null + } + } + + ## Widget CONTENT Directory should never exist because we deleted it but let's make sure it does ... + if ( -not (Test-Path -Path $orbAreaWidgetContentPath)) { + Write-Host "$loglead : Creating directory $orbAreaWidgetContentPath" + (New-Item -Path $orbAreaWidgetContentPath -ItemType Directory) | Out-Null + } + if ($contentSourcePathFound) { + $copyWidgetContentScript = { + param($sbSourceFolder, $sbOrbPath) + $sbSourcePath = Join-Path -Path $sbSourceFolder -ChildPath '*' + (Copy-Item -Path $sbSourcePath -Destination $sbOrbPath -Recurse -Force) | Out-Null + } + Write-Host "$loglead : $FILE_OPERATION_STRING CONTENT source folder: $contentSourceFolderPath to CONTENT destination path: $orbAreaWidgetContentPath" + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($contentSourceFolderPath, $orbAreaWidgetContentPath) -ScriptBlock $copyWidgetContentScript + } + + ## Widget LIB directory should never exist because we deleted it but let's make sure it does ... + if ( -not (Test-Path -Path $orbAreaBinPath)) { + Write-Host "$loglead : Creating directory $orbAreaBinPath" + (New-Item -Path $orbAreaBinPath -ItemType Directory) | Out-Null + } + Write-Host "$loglead : $FILE_OPERATION_STRING LIB source folder: $libSourceFolderPath to LIB destination path: $orbAreaBinPath" + $copyWidgetLibScript = { + param($sbSourceFolder, $sbOrbPath) + $sbSourcePath = Join-Path -Path $sbSourceFolder -ChildPath '*' + (Copy-Item -Path $sbSourcePath -Destination $sbOrbPath -Recurse -Force) | Out-Null + } + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($libSourceFolderPath, $orbAreaBinPath) -ScriptBlock $copyWidgetLibScript + } + + + if ($isRunning) { + foreach ($appPool in $iisApplicationPools) { + Write-Host "$loglead : Starting App Pool: $appPool" + (Start-WebAppPool -Name $appPool) | Out-Null + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebBinding.ps1 new file mode 100644 index 0000000..2bfed6c --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebBinding.ps1 @@ -0,0 +1,42 @@ +function New-AdminWebBinding { +<# +.SYNOPSIS + Create new admin web site binding +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$adminUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineAdminAppPools")] + [bool]$doCombineAdminAppPools + ) + + $logLead = (Get-LogLeadName); + $basePath = (Get-OrbPath); + $url = (Format-Url $adminUrl) + $adminSite = "WebClientAdmin" + $adminPath = (Join-Path $basePath "WebClientAdmin") + + Write-Verbose ("$logLead : Admin Website Read as {0}. Site path read as {1}" -f $url, $adminPath) + + if ($doCombineAdminAppPools) { + Write-Output ("$logLead : Combination flag present, setting application pool name to WebClientAdmin" -f $url) + $appPoolName = "WebClientAdmin" + } + + New-WebSite $adminPath $adminSite $appPoolName + + # Remove *:80:WebClientAdmin + if (Test-WebBinding $adminSite $adminSite) { + Remove-WebBinding $adminSite $adminSite + } + + New-WebBinding -Site $adminSite -Url $url + + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-AdminWebBinding -value New-AdminWebBinding; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebSite.ps1 new file mode 100644 index 0000000..becee58 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AdminWebSite.ps1 @@ -0,0 +1,30 @@ +function New-AdminWebSite { +<# +.SYNOPSIS + Create a new admin web site +#> + + [CmdletBinding()] + Param( + [string]$adminUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineAdminAppPools")] + [bool]$doCombineAdminAppPools + ) + + $logLead = (Get-LogLeadName); + $url = (Format-Url $adminUrl) + $adminPath = (Join-Path $basePath "WebClientAdmin") + Write-Verbose ("$logLead : Clean URL Read as {0}. Site path read as {1}" -f $url, $adminPath) + + if ($doCombineAdminAppPools) { + $appPoolName = "Admin" + Write-Output ("$logLead : Combination flag present, setting application pool name to Admin" -f $appPoolName) + } + + New-WebSite $adminPath $url $appPoolName + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-AdminWebSite -value New-AdminWebSite; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.ps1 new file mode 100644 index 0000000..3a672bd --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.ps1 @@ -0,0 +1,75 @@ +Function New-AppTierApplicationPool { +<# +.SYNOPSIS + Create a new application pool + TODO: This function should be deprecated as of April 2021 + +.DESCRIPTION + Install a Web Application Pool to the appropriate place. + Will ensure appropriate app pool exists + +.PARAMETER AppPoolName + [string] The name of the web application. + +.PARAMETER Credential + [PSCredential] The credentials to use for configuration here + +.PARAMETER IsGMSAAccount + [Switch] Is the account credential a GMSAAccount + +.INPUTS + AppPoolName and Credential are required. + +.OUTPUTS + Various diagnostic information about the install process + +.EXAMPLE + New-AppTierApplicationPool -AppPoolName BankService -SourcePath C:\Orb\BankService -IsLegacy + +Various diagnostic information about the install process. + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$AppPoolName, + + ## TODO: cbrand - 2019-08-19 - Replace the -Credential parameter with a call to (Get-AppServiceAccountName $appPool.WebAppName) if we will only use gMSA accounts or just embed that lookup below. + ## see also New-AppTierApplicationPools + ## This is to support SDK where we can use the same value without passing anything in for $username. That function knows how to support SDK username lookups. + ## We just need to do the right thing here when the username is blank (run as the default IISApplication user identity) + ## This almost certainly means we should consider a downgrade strategy where we take away the assigned app pool identity too. + [Parameter(Mandatory=$true, Position=1)] + [PSCredential]$Credential, + + [Parameter(Mandatory=$false, Position=2)] + [switch]$IsGMSAAccount + ) + process { + $logLead = (Get-LogLeadName) + + $appPoolPath = (Join-Path "IIS:\AppPools" $AppPoolName) + $appPool = (Get-Item $appPoolPath -ErrorAction SilentlyContinue) + + if ($null -eq $appPool) { + Write-Verbose "$logLead : Application Pool Not Found - $AppPoolName" + (New-WebAppPool -Name $AppPoolName) | Out-Null + (Set-AlkamiWebAppPoolConfiguration $AppPoolName) | Out-Null + Write-Host "$logLead : Application Pool Created - $AppPoolName" + } + + if ($Credential.Username -ne "REPLACEME") { + Write-Host "$logLead : Setting Application Pool Execution Account on $AppPoolName" + $value = @{userName=$Credential.UserName;identitytype=3} + + # the default expectation is gMSA, this is the exceptional case, might as well just overwrite the value then + if (!$IsGMSAAccount) { + $value = @{userName=$Credential.UserName;Password=(Get-PasswordFromCredential $Credential);identitytype=3} + } + + (Set-ItemProperty $appPoolPath -name processModel -value $value) | Out-Null + } else { + Write-Warning "$logLead : Value read as REPLACEME. AppPool $AppPoolName user will not be updated" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.tests.ps1 new file mode 100644 index 0000000..3860237 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPool.tests.ps1 @@ -0,0 +1,121 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-AppTierApplicationPool" { + $appPoolName = "FakeServiceApp" + + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return $null } #simulates not having a configured app setting + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return @{ not = "null";} } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiCredential -MockWith { param($username,$ignored) return @{ Username = $username; } } + Mock -ModuleName $moduleForMock -CommandName Set-ItemProperty -MockWith { } # We care that this gets called if $Credential.UserName -ne REPLACEME + Mock -ModuleName $moduleForMock -CommandName New-WebAppPool -MockWith { } # We care this gets called if Get-Item is not-null, and vice-versa + Mock -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -MockWith { } # We care this gets called if Get-Item is not-null, and vice-versa + + $fakeCredentialReplaceMe = New-Object 'Management.Automation.PsCredential' "REPLACEME",(ConvertTo-SecureString -AsPlainText -Force -String "Absolutely garbage string") + $fakeCredentialActualUser = New-Object 'Management.Automation.PsCredential' "ActualUser",(ConvertTo-SecureString -AsPlainText -Force -String "Absolutely garbage string") + + Context "Get-Item is not null, Username is REPLACEME" { + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return @{ not = "null";} } + + It "Does not throw" { + { New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount } | Should -Not -Throw + } + + It "Did not call Set-ItemProperty (due to username being REPLACEME)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ItemProperty -Scope It -Times 0 + } + + It "Did not call New-WebAppPool (due to apppool being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-WebAppPool -Scope It -Times 0 + } + + It "Did not call Set-AlkamiWebAppPoolConfiguration (due to apppool being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -Scope It -Times 0 + } + } + + Context "Get-Item is not null, Username is not REPLACEME" { + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return @{ not = "null"; } } + + It "Did not throw" { + { New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount } | Should -Not -Throw + } + + It "Did call Set-ItemProperty (due to username not being REPLACEME)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ItemProperty -Scope It -Times 1 + } + + It "Did not call New-WebAppPool (due to apppool being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-WebAppPool -Scope It -Times 0 + } + + It "Did not call Set-AlkamiWebAppPoolConfiguration (due to apppool being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -Scope It -Times 0 + } + } + + Context "Get-Item is null, Username is REPLACEME" { + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return $null } + + + It "Does not throw" { + { New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount } | Should -Not -Throw + } + + It "Did not call Set-ItemProperty (due to username being REPLACEME)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ItemProperty -Scope It -Times 0 + } + + It "Did call New-WebAppPool (due to apppool not being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-WebAppPool -Scope It -Times 1 + } + + It "Did call Set-AlkamiWebAppPoolConfiguration (due to apppool not being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialReplaceMe -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -Scope It -Times 1 + } + } + + Context "Get-Item is null, Username is not REPLACEME" { + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { return $null } + + + It "Does not throw" { + { New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount } | Should -Not -Throw + } + + It "Did call Set-ItemProperty (due to username not being REPLACEME)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-ItemProperty -Scope It -Times 1 + } + + It "Did call New-WebAppPool (due to apppool not being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-WebAppPool -Scope It -Times 1 + } + + It "Did call Set-AlkamiWebAppPoolConfiguration (due to apppool not being found)" { + New-AppTierApplicationPool -AppPoolName $AppPoolName -Credential $fakeCredentialActualUser -IsGMSAAccount + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -Scope It -Times 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.ps1 new file mode 100644 index 0000000..776c858 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.ps1 @@ -0,0 +1,22 @@ +function New-AppTierApplicationPools { +<# +.SYNOPSIS + This function sets the application tier application pools. + TODO: This function should be deprecated as of April 2021 +#> + [CmdletBinding()] + Param() + + foreach ($appPool in $global:appTierApplications) { + ## When we can get the bootstrap value set for Get-AppServiceAccountName we can remove this if block + ## TODO: cbrand ~ when https://jira.alkami.com/browse/SRE-12325 is completed we should remove this + if ([string]::IsNullOrWhiteSpace((Get-AppSetting "Environment.UserPrefix" -SuppressWarnings))) { + ## TODO: cbrand - 2019-08-19 - Replace the -Credential parameter with a call to (Get-AppServiceAccountName $appPool.WebAppName) or just embed that lookup in the call below if everything will be a GMSAAccount. + New-AppTierApplicationPool -AppPoolName $appPool.WebAppName -Credential (Get-AlkamiCredential $appPool.User $appPool.Password) -IsGMSAAccount:$appPool.IsGMSAAccount + } else { + New-AlkamiWebAppPool -appPoolName $appPool.WebAppName + } + } +} + +Set-Alias -name Create-AppTierApplicationPools -value New-AppTierApplicationPools; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.tests.ps1 new file mode 100644 index 0000000..36c35d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierApplicationPools.tests.ps1 @@ -0,0 +1,59 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-AppTierApplicationPools" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return $null } #simulates not having a configured app setting + Mock -ModuleName $moduleForMock -CommandName New-AppTierApplicationPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName New-AlkamiWebAppPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiCredential -MockWith { return New-Object 'Management.Automation.PsCredential' "REPLACEME",(ConvertTo-SecureString -AsPlainText -Force -String "Absolutely garbage string") } + + $global:appTierApplications = @( + @{ + Name = "FakeService"; WebAppName = "FakeService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "UltraFakeService.svc"; VIPSuffix="998"; + } + ) + + Context "Environment.UserPrefix has no value" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return $null } + + + It "Did not throw" { + { New-AppTierApplicationPools } | Should -Not -Throw + } + + It "Did call New-AppTierApplicationPool)" { + New-AppTierApplicationPools + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-AppTierApplicationPool -Scope It -Times 1 + } + + It "Did not call New-AlkamiWebAppPool" { + New-AppTierApplicationPools + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-AlkamiWebAppPool -Scope It -Times 0 + } + } + + Context "Environment.UserPrefix has some value" { + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return "this is a setting" } + + { New-AppTierApplicationPools } | Should -Not -Throw + + It "Did not throw" { + { New-AppTierApplicationPools } | Should -Not -Throw + } + + It "Did call New-AppTierApplicationPool)" { + New-AppTierApplicationPools + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-AppTierApplicationPool -Scope It -Times 0 + } + + It "Did not call New-AlkamiWebAppPool" { + New-AppTierApplicationPools + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-AlkamiWebAppPool -Scope It -Times 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierHostFileEntries.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierHostFileEntries.ps1 new file mode 100644 index 0000000..1c2fcfc --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierHostFileEntries.ps1 @@ -0,0 +1,51 @@ +function New-AppTierHostFileEntries { +<# +.SYNOPSIS + Create new app tier host file entries +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + $hostsContent = Get-HostsFileContent + $hostsToAdd = @() + + # Add hosts entry for everything except the multiplexer + # We have to figure out how to programatically find the VIP for SymConnect + foreach ($app in $appTierApplications | Where-Object {$_.Name -ne "SymConnectMultiplexer"}) { + # First try to resolve by name to see if DNS is handling + try { + $addresses = Get-IPAddressesForName $app.Name + } + catch { + # No action needed, check is below + $addresses = $null + } + + if (!(Test-IsCollectionNullOrEmpty $addresses)) { + Write-Output ("$logLead : IP Address for App {0} Resolved" -f $app.Name) + continue + } + + $hostsString = "127.0.0.1{0}{1}" + + # This is a redundant check since we're already checking DNS for the Service Name, but it doesn't hurt + if (($hostsContent | Where-Object {$_ -like ($hostsString -f "*", $app.Name)}).Count -gt 0) { + Write-Output ("$logLead : HostsEntry for {0} Already Exists" -f $app.Name) + continue + } + + $hostsToAdd += ($hostsString -f "`t`t", $app.Name) + } + + if ($hostsToAdd.Count -gt 0) { + Add-HostsFileContent $hostsToAdd -Force + } + else { + Write-Output "$logLead : No Host File Updates Required" + } +} + +Set-Alias -name Create-AppTierHostFileEntries -value New-AppTierHostFileEntries; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.ps1 new file mode 100644 index 0000000..922a1f1 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.ps1 @@ -0,0 +1,41 @@ +function New-AppTierWebApplications { +<# +.SYNOPSIS + Create app tier web applications for ORB applications from the global variable $global:appTierApplications + +.PARAMETER DefaultWebSiteName + The default web site name to use. This mostly only matters for SDK clients potentially. Defualts to "Default Web Site" +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [string]$DefaultWebSiteName = "Default Web Site" + ) + + $logLead = Get-LogLeadName + + $site = Get-WebSite $DefaultWebSiteName + + if ($null -eq $site) { + Write-Warning "$logLead : Could not find site $DefaultWebSiteName!" + Write-Host "$logLead : Make sure function Set-AppTierDefaultWebSite is called prior to attempting to create applications" + return + } + + foreach ($app in $global:appTierApplications) { + + $sourcePath = Join-Path -Path (Get-OrbPath) -ChildPath $app.WebAppName + if (!(Test-Path -Path $sourcePath)) { + Write-Warning "$logLead : Could not find source path: $sourcePath!" + continue + } + + if ($app.WebAppName -in (Get-KnownSkipWebAppNames)) { + Write-Warning "$logLead : Skipping $($app.WebAppName) because it is in Get-KnownSkipWebAppNames" + } else { + Install-AlkamiWebApplication -WebAppName $app.WebAppName -SourcePath $sourcePath -IsLegacy + } + } +} + +Set-Alias -name Create-AppTierWebApplications -value New-AppTierWebApplications; diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.tests.ps1 new file mode 100644 index 0000000..01c937b --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-AppTierWebApplications.tests.ps1 @@ -0,0 +1,73 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-AppTierWebApplications" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { "test" } + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + + Mock -ModuleName $moduleForMock -CommandName Get-WebSite -MockWith { return @{ Name = "testName"; } } + Mock -ModuleName $moduleForMock -CommandName Get-OrbPath -MockWith { return "C:\Orb" } + Mock -ModuleName $moduleForMock -CommandName Test-ShouldInstallExceptionService -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -MockWith { } + + $global:appTierApplications = @( + @{ + Name = "FakeService"; WebAppName = "FakeService"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Endpoint = "UltraFakeService.svc"; VIPSuffix = "998"; + } + ) + + Context "Website exists" { + + It "Did call Install-AlkamiWebApplication" { + { New-AppTierWebApplications } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -Scope It -Times 1 + } + } + + Context "Website does not exist" { + Mock -ModuleName $moduleForMock -CommandName Get-WebSite -MockWith { return $null } + + It "Did not call Install-AlkamiWebApplication" { + { New-AppTierWebApplications } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -Scope It -Times 0 + } + } + + Context "Source Path doesn't exist" { + + It "Did not call Install-AlkamiWebApplication" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $false } + New-AppTierWebApplications + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -Scope It -Times 0 + } + + It "Did call Write-Warning" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $false } + New-AppTierWebApplications + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Times 1 -ParameterFilter { $Message -eq "UUT : Could not find source path: C:\Orb\FakeService!" } + } + } + + Context "Source Path does exist" { + + It "Did not call Write-Warning" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + New-AppTierWebApplications + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Times 0 + } + + It "Did call Install-AlkamiWebApplication" { + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $true } + New-AppTierWebApplications + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-AlkamiWebApplication -Scope It -Times 1 -ParameterFilter { $SourcePath -eq "C:\Orb\FakeService" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebBinding.ps1 new file mode 100644 index 0000000..3b69f3a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebBinding.ps1 @@ -0,0 +1,42 @@ +function New-ClientWebBinding { +<# +.SYNOPSIS + Create new client web site bindings +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$clientUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineClientAppPools")] + [bool]$doCombineClientAppPools + ) + + $logLead = (Get-LogLeadName); + $basePath = (Get-OrbPath); + $url = (Format-Url $clientUrl) + $clientSite = "WebClient" + $clientPath = (Join-Path $basePath "WebClient") # basepath = C:\ORB + + Write-Verbose ("$logLead : Client Website Read as {0}. Site path read as {1}" -f $url, $clientPath) + + if ($doCombineClientAppPools) { + Write-Output ("$logLead : Combination flag present, setting application pool name to WebClient" -f $url) + $appPoolName = "WebClient" + } + + New-WebSite $clientPath $clientSite $appPoolName + + # Remove *:80:WebClient + if (Test-WebBinding $clientSite $clientSite) { + Remove-WebBinding $clientSite $clientSite + } + + New-WebBinding -Site $clientSite -Url $url + + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-ClientWebBinding -value New-ClientWebBinding; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebSite.ps1 new file mode 100644 index 0000000..4c61eb3 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-ClientWebSite.ps1 @@ -0,0 +1,31 @@ +function New-ClientWebSite { +<# +.SYNOPSIS + Create a new client web site +#> + + [CmdletBinding()] + Param( + [string]$clientUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineClientAppPools")] + [bool]$doCombineClientAppPools + ) + + $logLead = (Get-LogLeadName); + $url = (Format-Url $clientUrl) + $clientPath = (Join-Path $basePath "WebClient") + + Write-Verbose ("$logLead : Clean URL Read as {0}. Site path read as {1}" -f $url, $clientPath) + + if ($doCombineClientAppPools) { + Write-Output ("$logLead : Combination flag present, setting application pool name to WebClient" -f $url) + $appPoolName = "WebClient" + } + + New-WebSite $clientPath $url $appPoolName + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-ClientWebSite -value New-ClientWebSite; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-DefaultWebsite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-DefaultWebsite.ps1 new file mode 100644 index 0000000..1fdb801 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-DefaultWebsite.ps1 @@ -0,0 +1,58 @@ +function New-DefaultWebsite { +<# +.SYNOPSIS + Creates a site called "Default Web Site" + +.DESCRIPTION + When called, will attempt to force-create a site called "Default Web Site" +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("Name")] + [string]$DefaultWebsiteName = "Default Web Site" + ) + + $loglead = (Get-LogLeadName) + + $returnWebsite = (Get-Website -Name $DefaultWebsiteName) + if ($null -ne $returnWebsite) { + Write-Error "$logLead : A site called [$DefaultWebsiteName] already exists" + return $returnWebsite + } + + if ([string]::IsNullOrWhiteSpace($DefaultWebsiteName)) { + Write-Error "$logLead : no site name was passed into this function" + } + + Write-Verbose "$loglead : Using logic for force-creating `"Default Web Site`" with name '$DefaultWebsiteName'" + + $appPoolName = "DefaultAppPool" + + if ($DefaultWebsiteName -ne "Default Web Site") { + $appPoolName = $DefaultWebsiteName + } + + $appPool = (Get-Item (Join-Path "IIS:\AppPools" $appPoolName)) + + if ($null -eq $appPool) { + ## Couldn't find the application pool for this app, need to create one to go with this application + $appPool = New-AlkamiWebAppPool -Name $appPoolName + } + + $physicalPath = (Get-DefaultWebsiteDefaultPath) + + if (!(Test-Path $physicalPath)) { + Write-Verbose "$logLead : $physicalPath not found. Creating." + (New-Item -ItemType Directory -Path $physicalPath) | Out-Null + } + + Write-Host "Building the site [$DefaultWebsiteName] now in IIS" + ## Unfortunately there is an Alkami function with the exact same name. + $returnWebsite = WebAdministration\New-Website $DefaultWebsiteName -Force -IPAddress * -PhysicalPath $physicalPath -Port 80 -ApplicationPool $appPoolName + + ## Go ahead and optimize it since we just created it + Optimize-DefaultWebsite $returnWebsite + + return $returnWebsite +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebBinding.ps1 new file mode 100644 index 0000000..8318046 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebBinding.ps1 @@ -0,0 +1,42 @@ +function New-IPSTSWebBinding { +<# +.SYNOPSIS + Create new IPSTS web bindings +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$ipstsUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineIPSTSAppPools")] + [bool]$doCombineIPSTSAppPools + ) + + $logLead = (Get-LogLeadName); + $basePath = (Get-OrbPath); + $url = (Format-Url $ipstsUrl) + $ipstsSite = "IPSTS" + $ipstsPath = (Join-Path $basePath "IPSTS") + + Write-Verbose ("$logLead : IPSTS Website Read as {0}. Site path read as {1}" -f $url, $ipstsPath) + + if ($doCombineIPSTSAppPools) { + Write-Output ("$logLead : Combination flag present, setting application pool name to IPSTS" -f $url) + $appPoolName = "IPSTS" + } + + New-WebSite $ipstsPath $ipstsSite $appPoolName + + # Remove *:80:IPSTS + if (Test-WebBinding $ipstsSite $ipstsSite) { + Remove-WebBinding $ipstsSite $ipstsSite + } + + New-WebBinding -Site $ipstsSite -Url $url + + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-IPSTSWebBinding -value New-IPSTSWebBinding; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebSite.ps1 new file mode 100644 index 0000000..f04c53a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-IPSTSWebSite.ps1 @@ -0,0 +1,30 @@ +function New-IPSTSWebSite { +<# +.SYNOPSIS + Create a new IPSTS web site +#> + + [CmdletBinding()] + Param( + [string]$ipstsUrl, + + [Parameter(Mandatory = $false)] + [Alias("CombineIPSTSAppPools")] + [bool]$doCombineIPSTSAppPools + ) + + $logLead = (Get-LogLeadName); + $url = (Format-Url $ipstsUrl) + $ipstsPath = (Join-Path $basePath "IPSTS") + Write-Verbose ("$logLead : Clean URL Read as {0}. Site path read as {1}" -f $url, $ipstsPath) + + if ($doCombineIPSTSAppPools) { + Write-Output ("$logLead : Combination flag present, setting application pool name to IPSTS" -f $url) + $appPoolName = "IPSTS" + } + + New-WebSite $ipstsPath $url $appPoolName + New-WebTierHostFileEntries $url +} + +Set-Alias -name Create-IPSTSWebSite -value New-IPSTSWebSite; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.ps1 new file mode 100644 index 0000000..db032ce --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.ps1 @@ -0,0 +1,91 @@ +function New-WebBinding { +<# +.SYNOPSIS + Adds a new default web binding for IIS Sites + +.PARAMETER Site + The name of the site to add a binding for + +.PARAMETER Url + The url to use for the binding + +.PARAMETER AppPoolName + The app pool to use if it doesn't match the site or url. + Will default to the parameter for -Site if not provided +#> + [CmdletBinding()] + [OutputType([void])] + Param( + [Parameter(Mandatory = $true)] + [string]$Site, + + [Parameter(Mandatory = $true)] + [string]$Url, + + [Parameter(Mandatory = $false)] + [string]$AppPoolName = $null # may later be set to the value of the Site name if not provided. + ) + + $logLead = Get-LogLeadName + + if (Test-StringIsNullOrWhitespace -Value $AppPoolName) { + # If a specific app pool name was not passed in, we will use the site name to name it + $AppPoolName = $Site + } + + $appPool = Get-AlkamiWebAppPool $AppPoolName + if ($null -eq $appPool) { + $appPool = New-AlkamiWebAppPool $AppPoolName + } + + # We want to make sure the application pool settings are proper even if it's not new + (Set-AlkamiWebAppPoolConfiguration $AppPoolName) | Out-Null + + $mgr = Get-IISServerManager + + if ($null -eq $mgr.Sites[$Site]) { + Write-Warning "$logLead : Website $Site does not exist" + return + } else { + $httpBindingText = "*:80:$Url" + + # Requires IISAdministration 1.1.0.0 https://learn.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding?view=windowsserver2022-ps + # if ($null -eq (Get-IISSiteBinding -Site $Site -BindingInformation $httpBindingText -Protocol 'http')) { + if (Test-WebBinding -website $Site -url $Url) { + Write-Verbose "$logLead : Binding $httpBindingText already exists on IIS Site $Site" + } else { + Write-Host "$logLead : Creating Binding $httpBindingText on IIS Site $Site" + # Requires IISAdministration 1.1.0.0 https://learn.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding?view=windowsserver2022-ps + # New-IISSiteBinding -Name $Site -BindingInformation $httpBindingText -Protocol 'http' | Out-Null + [void] $mgr.Sites[$Site].Bindings.Add($httpBindingText, "http") + } + + $computerStore = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + $personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My + $certificate = Find-CertificateBySubjectOrSAN -Subject $Url -StoreLocation $computerStore -StoreName $personalStore + + if ($null -eq $certificate) { + Write-Warning "$logLead : Unable to find a certificate with subject or SAN which matches $Url. The SSL Binding must be created manually." + } else { + $sslBindingText = "*:443:$Url" + + # Requires IISAdministration 1.1.0.0 https://learn.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding?view=windowsserver2022-ps + # if ($null -eq (Get-IISSiteBinding -Site $Site -BindingInformation $sslBindingText -Protocol 'https')) { + if (Test-WebBinding -website $Site -url $Url -Ssl) { + Write-Verbose "$logLead : Binding $sslBindingText already exists on IIS Site $Site" + } else { + Write-Host "$logLead : Creating SSL binding $sslBindingText using certificate $($certificate.Subject)" + # Requires IISAdministration 1.1.0.0 https://learn.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding?view=windowsserver2022-ps + # New-IISSiteBinding -Name $Site -BindingInformation $sslBindingText -Protocol 'https' -CertificateThumbPrint $certificate.Thumbprint -SslFlag Sni -CertStoreLocation Cert:\$computerStore\$personalStore | Out-Null + [void] $mgr.Sites[$Site].Bindings.Add($sslBindingText, $certificate.GetCertHash(), $personalStore, [Microsoft.Web.Administration.SslFlags]::Sni) + } + } + } + + Write-Host "$logLead : Setting site to use application pool $AppPoolName" + $mgr.Sites[$Site].ApplicationDefaults.ApplicationPoolName = $AppPoolName + + Save-IISServerManagerChanges $mgr +} + +Set-Alias -name Create-WebBinding -value New-WebBinding; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.tests.ps1 new file mode 100644 index 0000000..9bea23d --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-WebBinding.tests.ps1 @@ -0,0 +1,45 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-WebBinding" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Save-IISServerManagerChanges -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Test-WebBinding -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiWebAppPool -MockWith { return @{ not = "null" } } + Mock -ModuleName $moduleForMock -CommandName New-AlkamiWebAppPool -MockWith { return @{ not = "null" } } + Mock -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Test-StringIsNullOrWhitespace -MockWith { return $false } + + # Requires IISAdministration 1.1.0.0 https://learn.microsoft.com/en-us/powershell/module/iisadministration/new-iissitebinding?view=windowsserver2022-ps + # Mock -ModuleName $moduleForMock -CommandName New-IISSiteBinding -MockWith { } + # When use with New-IISSiteBinding and returning null/not-null we can test New-IISSiteBinding was called once or twice + Mock -ModuleName $moduleForMock -CommandName Find-CertificateBySubjectOrSAN -MockWith { return $null } + + Mock -ModuleName $moduleForMock -CommandName Get-IISServerManager -MockWith { + $bindings = @{} + $bindings | Add-Member -MemberType ScriptMethod -Name Add -Value {} -Force + $serverManager = @{ + Sites = @{ FakeSite = @{ Bindings = $bindings; ApplicationDefaults = @{ ApplicationPoolName = ""; }; }; } + } + + return $serverManager + } + + Context "Basic test" { + + It "Did not throw and Called Set-AlkamiWebAppPoolConfiguration" { + { New-WebBinding -site "FakeSite" -url "https://example.com" -appPoolName "FakeSite" } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AlkamiWebAppPoolConfiguration -Scope It -Times 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-WebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-WebSite.ps1 new file mode 100644 index 0000000..b54f709 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-WebSite.ps1 @@ -0,0 +1,78 @@ +function New-WebSite { +<# +.SYNOPSIS + Create a new website using Alkami default patterns + +.PARAMETER SitePath + The path to use for the site. Example C:\Inetpub\wwwroot + +.PARAMETER Url + The URL to assign to the site. If a cert is found, will bind to 80 and 443. If no certificate is found, just port 80. + +.PARAMETER AppPoolName + Optional app pool name. Useful for consolidating app pools. +#> +## TODO: cbrand ~ Can we rename this file so it doesn't conflict with "traditional" Microsoft module naming? Maybe "New-AlkamiWebSite" + [CmdletBinding()] + [OutputType([void])] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$SitePath, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Url, + + [Parameter(Mandatory = $false)] + [string]$AppPoolName + ) + + $logLead = Get-LogLeadName + + if ([String]::IsNullOrEmpty($AppPoolName)) { + Write-Host "$logLead : No AppPoolName was provided. Using Url for AppPoolName" + $AppPoolName = $Url + } + + $mgr = Get-IISServerManager + + if ($null -ne $mgr.Sites[$Url]) { + Write-Host "$logLead : Website [$Url] already exists" + $newSite = $mgr.Sites[$Url] + } else { + Write-Host "$logLead : Creating Website [$Url]" + + $httpBindingText = "*:80:$Url" + Write-Host "$logLead : Creating Site with binding [$httpBindingText] in path [$SitePath]" + $newSite = $mgr.Sites.Add($Url, "http", $httpBindingText, $SitePath) + + ## Removed dependecy on Alkami.Ops module + $computerStore = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + $personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My + $certificate = Find-CertificateBySubjectOrSAN -Subject $Url -StoreLocation $computerStore -StoreName $personalStore + + if ($null -eq $certificate) { + Write-Warning "$logLead : Unable to find a certificate with subject or SAN which matches [$Url]. The SSL Binding must be created manually." + } else { + $sslBindingText = "*:443:$Url" + Write-Host "$logLead : Creating SSL binding [$sslBindingText] using certificate [$($certificate.Subject)]" + $newSite.Bindings.Add($sslBindingText, $certificate.GetCertHash(), $personalStore, [Microsoft.Web.Administration.SslFlags]::Sni) | Out-Null + } + } + + if ($null -eq $mgr.ApplicationPools[$AppPoolName]) { + Write-Host "$logLead : Creating application pool [$AppPoolName]" + $mgr.ApplicationPools.Add($AppPoolName) | Out-Null + } + + Write-Host "$logLead : Setting site to use application pool [$AppPoolName]" + $newSite.ApplicationDefaults.ApplicationPoolName = $AppPoolName + + Save-IISServerManagerChanges -ServerManager $mgr + + # We want to make sure the application pool settings are proper even if it's not new + Write-Host "$logLead : Configuring application pool [$AppPoolName] with default values" + (Set-AlkamiWebAppPoolConfiguration $AppPoolName) | Out-Null +} + +Set-Alias -name Create-WebSite -value New-WebSite \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/New-WebTierWebApplications.ps1 b/Modules/Alkami.PowerShell.IIS/Public/New-WebTierWebApplications.ps1 new file mode 100644 index 0000000..0a2827a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/New-WebTierWebApplications.ps1 @@ -0,0 +1,28 @@ +function New-WebTierWebApplications { +<# +.SYNOPSIS + This function creates directories for some ORB web applications +#> + + [CmdletBinding()] + param() + + $basePath = (Get-OrbPath) + + $webApplications = @( + @{ Name = "ORBFX"; Path = (Join-Path $basePath "ORBFX"); }, + @{ Name = "CUFX"; Path = (Join-Path $basePath "CUFX"); }, + @{ Name = "TextBanking"; Path = (Join-Path $basePath "TextBanking"); } + ) + + foreach ($webApp in $webApplications) { + ## Until we get these applications converted to the new installer pattern, we need to ensure the folders exist + if (!(Test-Path $webApp.Path)) { + New-Item -ItemType Directory -Path $webApp.Path | Out-Null + } + #Install-WebApplication -WebAppName $webApp.Name -SourcePath $webApp.Path -IsClient -NeedsShared + Write-Host "Skipping app install of $($webApp.Name)" + } +} + +Set-Alias -name Create-WebTierWebApplications -value New-WebTierWebApplications \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.Tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.Tests.ps1 new file mode 100644 index 0000000..222955f --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.Tests.ps1 @@ -0,0 +1,313 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +describe -Tag 'Unit' 'Open-AlkamiSites' { + + Context "Default Mocks" { + + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { + + $adminBindings = @( + [PsCustomObject] + @{ + host = "admin.fooSite.com" + protocol = "http" + }, + + [PsCustomObject] + @{ + host = "admin.barSite.com" + protocol = "https" + }) + + $sites = @( [PsCustomObject]@{ + Bindings = $adminBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $adminBindings + State = "Stopped" + } + ) + return $sites; + } -ParameterFilter { $adminSitesOnly -and $adminSitesOnly -eq $true } + + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { + + $adminBindings = @( + [PsCustomObject] + @{ + host = "admin.fooSite.com" + protocol = "http" + }, + + [PsCustomObject] + @{ + host = "admin.barSite.com" + protocol = "https" + }) + + $bindings = @( + [PsCustomObject] + @{ + host = "www.fooSite.com" + protocol = "http" + }, + + [PsCustomObject] + @{ + host = "www.barSite.com" + protocol = "https" + }) + + $sites = @( [PsCustomObject]@{ + Bindings = $adminBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $adminBindings + State = "Stopped" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Stopped" + } + ) + return $sites; + } + + Mock -ModuleName $moduleForMock Open-UrlInDefaultBrowser -MockWith { return $null } + + it "should open sites" { + + $openedSites = Open-AlkamiSites -adminSitesOnly -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + $openedSites.Count | should -BeGreaterThan 0 + } + + it "should get random sites when Random == true" { + Mock -ModuleName $moduleForMock Get-Random -MockWith { return $null } + + Open-AlkamiSites -Random + + Assert-MockCalled -ModuleName $moduleForMock Get-Random + } + + it "should only open https sites" { + + $openedSites = Open-AlkamiSites -adminSitesOnly -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + + $openedSites.Count | should -BeGreaterThan 0 + + foreach ($site in $openedSites) { + $site.protocol | should -Be "https" + } + } + + it "should only open Admin Sites when AdminOnly == true" { + + $openedSites = Open-AlkamiSites -adminSitesOnly -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + + $openedSites.Count | should -BeGreaterThan 0 + + foreach ($site in $openedSites) { + $site.host | should -Match "admin" + } + } + + it "should open all sites when AdminOnly == false" { + + $openedSites = Open-AlkamiSites -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + $countNonAdminSites = 0 + + foreach ($site in $openedSites) { + if ($site.host -match "www") { + $countNonAdminSites++ + } + } + + $countNonAdminSites | should -BeGreaterThan 0 + + } + + it "should not return sites without the returnSites switch set" { + $openedSites = Open-AlkamiSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + $openedSites | should -BeNullOrEmpty + } + } + + Context "Custom Mocks" { + Mock -ModuleName $moduleForMock Open-UrlInDefaultBrowser -MockWith { return $null } + + it "should not open sites if none exist" { + + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { return $null } + + $openedSites = Open-AlkamiSites -returnSites + + $openedSites.Count | should -Be 0 + + } + + it "should not open sites if no https sites exist" { + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { + + $adminBindings = @( + [PsCustomObject] + @{ + host = "admin.fooSite.com" + protocol = "http" + }) + + $bindings = @( + [PsCustomObject] + @{ + host = "www.fooSite.com" + protocol = "http" + }) + + $sites = @( [PsCustomObject]@{ + Bindings = $adminBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $adminBindings + State = "Stopped" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Stopped" + } + ) + return $sites; + } + $openedSites = Open-AlkamiSites -returnSites + + $openedSites.Count | should -Be 0 + + } + + it "should only open sites which match the filter when it's provided" { + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { + + $adminBindings = @( + [PsCustomObject] + @{ + host = "admin.fooSite.com" + protocol = "https" + }, + + [PsCustomObject] + @{ + host = "admin.barSite.com" + protocol = "https" + }) + + $bindings = @( + [PsCustomObject] + @{ + host = "www.fooSite.com" + protocol = "https" + }, + + [PsCustomObject] + @{ + host = "www.barSite.com" + protocol = "https" + }) + + $sites = @( [PsCustomObject]@{ + Bindings = $adminBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $adminBindings + State = "Stopped" + }, + [PsCustomObject]@{ + Bindings = $bindings + State = "Stopped" + } + ) + return $sites; + } + + $openedSites = Open-AlkamiSites -filter "foo" -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + $openedSites.Count | should -BeGreaterThan 0 + + foreach ($site in $openedSites) { + Write-Host $site.host + $site.host | should -Not -Match "bar" + } + } + + it "should only open Started sites" { + Mock -ModuleName $moduleForMock Get-IisSiteList -MockWith { + + $startedBindings = @( + [PsCustomObject] + @{ + host = "www.startedSite.com" + protocol = "https" + } + ) + $stoppedBindings = @( + [PsCustomObject] + @{ + host = "www.stoppedSite.com" + protocol = "https" + } + ) + $sites = @( [PsCustomObject]@{ + Bindings = $startedBindings + State = "Started" + }, + [PsCustomObject]@{ + Bindings = $stoppedBindings + State = "Stopped" + } + ) + return $sites; + } + + $openedSites = Open-AlkamiSites -adminSitesOnly -returnSites + + Assert-MockCalled -ModuleName $moduleForMock Open-UrlInDefaultBrowser + $openedSites.Count | should -BeGreaterThan 0 + + foreach ($site in $openedSites) { + Write-Host $site.host + Write-Host $site.State + $site.host | should -Match "started" + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.ps1 new file mode 100644 index 0000000..90c88d5 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Open-AlkamiSites.ps1 @@ -0,0 +1,128 @@ +function Open-AlkamiSites { +<# +.SYNOPSIS + Opens Admin and WebClient Sites in the User's Default Browser + +.DESCRIPTION + Opens Admin and WebClient Sites in the User's Default Browser + Supports opening admin sites only for VAU, sites based on a regex filter, or a random Client/Admin site set + +.PARAMETER adminSitesOnly + [switch] Opens only Admin sites on the server + +.PARAMETER randomSite + [switch] Opens a random Admin / WebClient URL from each matching site + +.PARAMETER filter + [string] A regex string used to match the site binding URI. Opens matching sites + +.EXAMPLE + Open-AlkamiSites +PS C:\Users\dsage> Open-AlkamiSites +[Open-AlkamiSites] : Opening URL https://myaccounts.bethpagefcu.com in default browser +[Open-AlkamiSites] : Opening URL https://myaccounts.secumd.org in default browser +[Open-AlkamiSites] : Opening URL https://myaccounts.bellco.org in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.bethpagefcu.com in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.secumd.org in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.bellco.org in default browser + +.EXAMPLE + Open-AlkamiSites -adminSitesOnly + +PS C:\Users\dsage> Open-AlkamiSites -adminSitesOnly +[Open-AlkamiSites] : Opening URL https://myadmin.bethpagefcu.com in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.secumd.org in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.bellco.org in default browser + +.EXAMPLE + Open-AlkamiSites -random + +PS C:\Users\dsage> Open-AlkamiSites -random +[Open-AlkamiSites] : Grabbing random entry for site WebClient +[Open-AlkamiSites] : Opening URL https://myaccounts.bethpagefcu.com in default browser +[Open-AlkamiSites] : Grabbing random entry for site WebClientAdmin +[Open-AlkamiSites] : Opening URL https://myadmin.secumd.org in default browser + +.EXAMPLE + Open-AlkamiSites -filter bellco + +PS C:\Users\dsage> Open-AlkamiSites -filter bellco +[Open-AlkamiSites] : Opening URL https://myaccounts.bellco.org in default browser +[Open-AlkamiSites] : Opening URL https://myadmin.bellco.org in default browser +#> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + [Parameter(Mandatory = $false)] + [Alias("AdminOnly")] + [switch]$adminSitesOnly, + + [Parameter(Mandatory = $false)] + [Alias("Random")] + [switch]$randomSite, + + [Parameter(Mandatory = $false)] + [string]$filter = "", + + [Parameter(Mandatory = $false)] + [switch]$returnSites + + ) + + $logLead = (Get-LogLeadName); + + try { + $sites = Get-IisSiteList -adminSitesOnly:$adminSitesOnly + } catch [System.Management.Automation.RuntimeException] { + Write-Warning "$logLead : Could not find any WebClient or WebClientAdmin sites on this server" + return + } + + $openedSites = @() + $sites | Where-Object { $_.State -eq "Started" } | ForEach-Object { + + # Grab only the sites with HTTPS bindings (applying filter if applicable). + $httpsSites = $_.Bindings | Where-Object { $_.Protocol -like "https" } | Where-Object { $_.Host -match $filter } + + # Did we find any? + if ($null -eq $httpsSites) { + # Nope. Was it more than likely a problem with their filter? + if ($null -ne $filter) { + # Show the user their filter with the warning. + Write-Warning ("$logLead : Could not find any https bindings for site {0} that matches filter '{1}'" -f $_.Name, $filter) + } else { + # Just show the user the warning. + Write-Warning ("$logLead : Could not find any https bindings for site {0}" -f $_.Name) + } + } else { + # Are we randomly selecting the site from the list? + if ($randomSite.IsPresent) { + + # Log what we're doing. + Write-Host ("$logLead : Grabbing random entry for site {0}" -f $_.Name) + + # Grab a random object from the list. + [array]$httpBindings = Get-Random -InputObject $httpsSites + } else { + + # Log what we're doing. + Write-Verbose ("$logLead : Opening each HTTPS binding for site {0}" -f $_.Name) + + # Grab the first object from the list. + [array]$httpBindings = $httpsSites + } + + # Build the URL, log it, and open each matching binding. + foreach ($httpBinding in $httpBindings) { + + $urlString = "{0}://{1}" -f $httpBinding.Protocol, $httpBinding.Host + Write-Host ("$logLead : Opening URL {0} in default browser" -f $urlString) + Open-UrlInDefaultBrowser $urlString | Out-Null + $openedSites += $httpBindings + } + } + } + if ($returnSites.IsPresent) { + return $openedSites + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Optimize-DefaultWebsite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Optimize-DefaultWebsite.ps1 new file mode 100644 index 0000000..f8a77d1 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Optimize-DefaultWebsite.ps1 @@ -0,0 +1,110 @@ +Function Optimize-DefaultWebsite { +<# +.SYNOPSIS + This function is used to ensure a consistent Default Web Site on a developers through production instance of IIS. + +.DESCRIPTION + This function is used to ensure a consistent Default Web Site on a developers through production instance of IIS. + This function will do the following to the passed in site: + * ensure protocols and bindings are set correctly for http + * ensure protocols and bindings are set correctly for net.tcp + * add protocls and bindings if necessary for http + * add protocls and bindings if necessary for net.tcp + This function should not be used on non-default-web-site sites. + This function has no way to know if a site is valid or not for this optimization. + +.PARAMETER Website + The default website being optimized + +.INPUTS + We require the website we want to run against to be passed in. + +.OUTPUTS + Returns the updated website once optimized. + +.EXAMPLE + Optimize-DefaultWebsite $website + +Optimize-DefaultWebsite $website + +#> + [CmdletBinding(DefaultParameterSetName = 'WebsiteName')] + param( + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'WebsiteObject')] + [ValidateNotNull()] + [Object]$Website, + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'WebsiteName')] + [ValidateNotNull()] + [string]$WebsiteName + ) + + $loglead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'WebsiteObject') { + $WebsiteName = $Website.Name + } + + $foundNetTcp = $false + $foundHttp = $false + $foundSimpleHttpBinding = $false + $foundWildcardHttpBinding = $false + + ## Cannot add duplicate collection entry of type 'binding' with combined key attributes 'protocol, bindingInformation' respectively set to 'net.tcp, 808:*' + $bindingCollection = (Get-ItemProperty IIS:\Sites\$($WebsiteName) -Name bindings).Collection + foreach ($binding in $bindingCollection) { + if ($binding.protocol -eq 'net.tcp') { + if ($binding.bindingInformation -ne '808:*') { + throw "$loglead : Found a net.tcp binding against a non-default port (not 808). Can not continue. Please contact Alkami SDK Support for more details." + } + $foundNetTcp = $true + } + if ($binding.protocol -eq 'http') { + if ($binding.bindingInformation -eq '*:80:*') { + $foundWildcardHttpBinding = $true + } + if ($binding.bindingInformation -eq '*:80:') { + $foundSimpleHttpBinding = $true + } + $foundHttp = $true + } + } + + $protocolUpdateRequired = $false + $protocols = (Get-ItemProperty IIS:\Sites\$($WebsiteName) -Name EnabledProtocols).enabledProtocols + if ($protocols -notmatch 'http') { + Write-Verbose "$loglead : New protocol http" + + $protocols = 'http,' + $protocols + $protocolUpdateRequired = $true + } + if ($protocols -notmatch 'net.tcp') { + Write-Verbose "$loglead : New protocol net.tcp" + $protocols += ',net.tcp' + $protocolUpdateRequired = $true + } + + ## the above may force a double comma because this is easier for me to do + $protocols = $protocols -replace ',,',',' + + if (!$foundNetTcp) { + Write-Verbose "$loglead : Set bindings net.tcp" + (New-ItemProperty IIS:\Sites\$($WebsiteName) -Name bindings -Value @{protocol="net.tcp"; bindingInformation="808:*"} -ErrorAction Ignore) | Out-Null + } + + if (!$foundHttp) { + Write-Verbose "$loglead : New bindings http" + (New-ItemProperty IIS:\Sites\$($WebsiteName) -Name bindings -Value @{protocol="http"; bindingInformation="*:80:"} -ErrorAction Ignore) | Out-Null + } + if ($foundSimpleHttpBinding -and $foundWildcardHttpBinding) { + ## TODO: Do we need to remove one or the other? + } + + if ($protocolUpdateRequired) { + Write-Verbose "$loglead : Set EnabledProtocols" + (Set-ItemProperty IIS:\Sites\$($WebsiteName) -Name EnabledProtocols -Value $protocols -ErrorAction Ignore) | Out-Null + } + + (Set-ItemProperty "IIS:\Sites\$($WebsiteName)" -Name applicationDefaults.preloadEnabled -Value $true) | Out-Null + + return (Get-Website -Name $WebsiteName) +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSites.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSites.ps1 new file mode 100644 index 0000000..60a4346 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSites.ps1 @@ -0,0 +1,199 @@ +function Ping-AlkamiWebSites { + <# +.SYNOPSIS + Warms up web tier web services +#> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + [Parameter(Mandatory = $false)] + [Alias("SkipIPSTS")] + [switch]$doSkipIPSTS, + + # This can be used to consume the output as an object in a downstream function + # If not included the output is formatted for review by a human + [Parameter(Mandatory = $false)] + [Alias("NoOutput")] + [switch]$skipOutput, + + [Parameter(Mandatory = $false)] + [Alias("SkipServerCheck")] + [switch]$skipCheck + ) + + $logLead = (Get-LogLeadName) + + if ((Test-IsAppServer) -and !($skipCheck.IsPresent)) { + # Exit Early + Write-Host ("$logLead : This is not a valid function for an app server") + return + } + + $siteArray = @() + $testPattern = "text/javascript" + $maxJobs = 3 + $jobs = @() + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + if ($doSkipIPSTS) { + $sites = $mgr.Sites | Where-Object { $_.State -eq "Started" } | Where-Object { $_.Applications["/"].VirtualDirectories["/"].PhysicalPath -notlike "*IPSTS" -and $_.Name -notlike "*Eagle*" } + } else { + $sites = $mgr.Sites | Where-Object { $_.State -eq "Started" } | Where-Object { $_.Name -notlike "*Eagle*" } + } + + $sites = @($sites) + if (@($sites).Length -eq 0) { + Write-Warning "No sites were found to install!!" + Write-Warning "Please make sure the ConfigurationValues.ps1 got updated correctly!!" + return + } + + $sites | Where-Object { $_.Name -ne "Default Web Site" } | ForEach-Object { + + $httpBinding = $_.Bindings | Where-Object { $_.Protocol -like "https" } | Select-Object -First 1 + + if ($null -eq $httpBinding) { + Write-Warning ("$logLead : Could not find any https binding for site {0}" -f $_.Name) + } else { + $hostName = "localhost" + if ($httpBinding.Protocol -eq "https") { + $hostName = $httpBinding.Host + } + + $urlString = "{0}://{1}" -f $httpBinding.Protocol, $hostName, $_.Name + + # IPSTS has to be tested a different way + if ($_.Applications["/"].VirtualDirectories["/"].PhysicalPath -like "*IPSTS") { + # We have to fish for the admin site URL here to construct the IPSTS test URL + $domainArray = $urlString.Split(".") | Select-Object -Last 2 + $domainString = $domainArray -join "." + $adminSites = $sites | Where-Object { $_.Applications["/"].VirtualDirectories["/"].PhysicalPath -like "*Admin" } + $adminMatch = $adminSites | Where-Object { $_.Bindings | Where-Object { $_.Host -like "*$domainString" } } | Select-Object -First 1 + + if ($null -eq $adminMatch) { + # We couldn't match on admin, so we'll at least warm up the site, but it'll fail + $siteArray += @{Name = $_.Name; Url = $urlString; Ipsts = $true; Admin = $false; Client = $false } + } else { + # Craft the IPSTS test URL using the admin match. + # The page that will actually be loaded is admin, via IPSTS + $adminHttpsBinding = $adminMatch.Bindings | Where-Object { $_.Protocol -like "https" } + + $adminHostName = "localhost" + if ($adminHttpsBinding.Protocol -eq "https") { + $adminHostName = $adminHttpsBinding.Host + } + + # Generate a GUID to add as the activationId + $guid = [System.GUID]::NewGuid() + + $adminMatchUrlString = "{0}://{1}" -f $httpBinding.Protocol, $adminHostName + $siteArray += @{Name = $_.Name; Url = $urlString + "?wa=wsignin1.0&wtrealm=" + $adminMatchUrlString + "&activationId=" + $guid; Ipsts = $true; Admin = $false; Client = $false } + } + } elseif ($_.Applications["/"].VirtualDirectories["/"].PhysicalPath -like "*Admin") { + $siteArray += @{Name = $_.Name; Url = $urlString; Ipsts = $false; Admin = $true; Client = $false } + } else { + # Add Site to Cover CUFX / ORBFX / Mobile Auth + $siteArray += @{Name = $_.Name; Url = $urlString; Ipsts = $false; Admin = $false; Client = $true } + $cleanUrl = $urlString.TrimEnd("/") + $siteArray += @{Name = $_.Name; Url = ($cleanUrl + "/CUFX"); Ipsts = $false; Admin = $false; Client = $true } + $siteArray += @{Name = $_.Name; Url = ($cleanUrl + "/ORBFX"); Ipsts = $false; Admin = $false; Client = $true } + $siteArray += @{Name = $_.Name; Url = ($cleanUrl + "/Mobile/Authentication"); Ipsts = $false; Admin = $false; Client = $true } + } + } + } + + $scriptBlock = { + param ($site, $testPattern) + + try { + Add-Type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ + + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + $response = Invoke-WebRequest -Uri $site.Url -UseBasicParsing + $stopWatch.Stop() + + if ($response.StatusCode -ne 200 -or $response.Content -notmatch $testPattern) { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $false + StatusCode = $response.StatusCode + Elapsed = $stopWatch.Elapsed.ToString() + } + } else { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $true + StatusCode = "200" + Elapsed = $stopWatch.Elapsed.ToString() + } + } + } catch { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $false + StatusCode = "Error" + Elapsed = $stopWatch.Elapsed.ToString() + } + } + } + + $siteResults = @() + if (!(Test-IsCollectionNullOrEmpty $siteArray)) { + Write-Host ("$logLead : Starting Site Warmup") + foreach ($site in $siteArray) { + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $site, $testPattern + $running = @($jobs | Where-Object { $_.State -eq 'Running' }) + + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) { + (Wait-Job -Job $jobs -Any) | Out-Null + $running = @($jobs | Where-Object { $_.State -eq 'Running' }) + } + } + + Wait-Job -Job $jobs > $null + + $failed = @($jobs | Where-Object { $_.State -eq 'Failed' }) + if ($failed.Count -gt 0) { + $failed | ForEach-Object { $_.ChildJobs[0].JobStateInfo.Reason.Message } + } + + $jobs | ForEach-Object { + $siteResults += $_ | Receive-Job | Select-Object URL, Success, StatusCode, Elapsed + } + + $functionStopWatch.Stop() + + if ($skipOutput) { + return $siteResults + } else { + if ($null -ne ($siteResults | Where-Object { $_.Success -eq $false })) { + Write-Warning ("$logLead : One or more URLs failed the test case:`n") + } + + $results = ($siteResults | Sort-Object -Property Success | Format-Table -Property @{Label = "URL"; Width = 70; e = { $_.URL }; Alignment = "Left" }, @{Label = "Success"; Width = 15; e = { $_.Success }; Alignment = "Right" }, @{Label = "Elapsed"; Width = 25; e = { $_.Elapsed }; Alignment = "Right" } | Out-String) + Write-Host $results + Write-Host ("Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString()) + + return $siteResults + } + } else { + Write-Host "No sites Configured, skipping warmup" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSitesExtended.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSitesExtended.ps1 new file mode 100644 index 0000000..d1dfe17 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Ping-AlkamiWebSitesExtended.ps1 @@ -0,0 +1,312 @@ +function Ping-AlkamiWebSitesExtended { +<# +.SYNOPSIS + Warms up web tier web services +#> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + # This can be used to consume the output as an object in a downstream function + # If not included the output is formatted for review by a human + [Parameter(Mandatory = $false)] + [Alias("NoOutput")] + [switch]$skipOutput, + + [Parameter(Mandatory = $false)] + [Alias("SkipServerCheck")] + [switch]$skipCheck, + [array]$endpoints = @("AcculynkP2PSSO", + "ACHPayments", + "API", + "Applications", + "Authentication", + "AutoBooksSSO", + "AvokaSso", + "Azigio", + "BaxterVCA", + "Benefits", + "BenefitsBCU", + "BillPay", + "BillpayV2", + "BizCardz", + "Budgets", + "BusinessACH", + "BusinessAdmin", + "BusinessAdministration", + "BusinessReports", + "BusinessWires", + "CalculatorCalendar", + "CardManagement", + "CardManagementV2", + "CardRewards", + "CardWorks", + "Cashback", + "CashEdge", + "CenlarMortgage", + "CheckFreeBillPay", + "CheckFreeBusinessSSO", + "CheckFreeSSOV2", + "Content", + "CourtesyPay", + "CourtesyPayV2", + "COWWW", + "CUBUSEStatementAlert", + "CUBUSLoyaltyChecking", + "CUBUSRewards", + "CUBUSSkipAPay", + "CubusSkipAPaySSO", + "CUDL", + "CunexusLoanEngine", + "Dashboard", + "DashboardV2", + "DFCUApplications", + "DirectDeposit", + "Dispute", + "DMIMortgage", + "DpxPay", + "DraftServices", + "DSCardOrder", + "DSCMNDonations", + "DSCopyRequest", + "DSEStatements", + "DSMarketingEmail", + "DSPrivilegePay", + "DSSkipAPay", + "DYOC", + "eDocs", + "EDocuments", + "EnsentaSSO", + "FicoScore", + "FICSMortgage", + "FISSSO", + "FISSSOV2", + "Forms", + "FuegoCardManagement", + "FuegoLoan", + "Geezeo", + "GenericUrlLaunch", + "HighQSavings", + "IMSI", + "ImsiSso", + "InstantOpen", + "InstantOpenSSO", + "Investments", + "IPay", + "IPAYSSOV2", + "Iris", + "KaneSecureForms", + "LaserTec", + "LoanCoupon", + "Locations", + "MasterCardRewards", + "MemberServices", + "MeridianLink", + "MessageCenter", + "MoveMoney", + "MyAccounts", + "MyAccountsV2", + "MyCardInfo", + "Oauth", + "OauthExtensionExample", + "oFlows", + "OpenAnywhere", + "OracleKnowledgeBase", + "ORCASInvestments", + "OverdraftProtectionPriority", + "Payroll", + "PcusCallback", + "PFCULoanApplications", + "PointsForPerks", + "PositivePayACHAlert", + "ProfitStarsCommercialRDC", + "ProfitStarsRDC", + "ProPay", + "PSCUAccessPoint", + "PscuBillPay", + "PSCUMFoundryEnrollment", + "PSCURewards", + "QBO", + "QuickApply", + "QuickApps", + "QuickWires", + "QuorumSecureForms", + "QuorumSecureFormsFormsAndServices", + "QuorumSecureFormsOnlineDeposits", + "QuorumSecureFormsOpenNewAccounts", + "QuorumSecureFormsWireTransfers", + "RemoteDeposit", + "ResearchAndPlanning", + "RetailWires", + "RewardsMacu", + "RewardsNow", + "RTSRewards", + "SavingsGoals", + "SavvyMoney", + "Settings", + "SkipAPay", + "SkipAPayV2", + "STCUBalanceTransfer", + "STCULending", + "StudentChoiceLoan", + "Swbc", + "Sweeps", + "SymAppSSO", + "SymitarLoans", + "TeleSignCallBack", + "Transfer", + "TransferV2", + "UChooseRewards", + "uOpen", + "UpdateSecurityCode", + "UserServices", + "USFBenefits", + "Vantiv", + "VertifiDeposZipSSO", + "VirtualCapture", + "YodleeSSO" + ) + ) + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName + + $logLead = (Get-LogLeadName); + + if ((Test-IsAppServer) -and !($skipCheck.IsPresent)) { + # Exit Early + Write-Host ("$logLead : This is not a valid function for an app server") + return + } + + $siteArray = @() + $testPattern = "text/javascript" + $maxJobs = 15 + $jobs = @() + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + + $sites = $mgr.Sites | Where-Object {$_.State -eq "Started"} | Where-Object {$_.Applications["/"].VirtualDirectories["/"].PhysicalPath -notlike "*IPSTS" -and $_.Name -notlike "*Eagle*" -and $_.Name -notlike "*admin*" -and $_.Name -ne "Default Web Site"} + + + $sites = @($sites); + if (@($sites).Length -eq 0) { + Write-Warning "$logLead : No sites were found to install!!"; + Write-Warning "$logLead : Please make sure the ConfigurationValues.ps1 got updated correctly!!"; + return; + } + + $sites | ForEach-Object { + + $httpBinding = $_.Bindings | Where-Object {$_.Protocol -like "https"} | Select-Object -First 1 + + if ($null -eq $httpBinding) { + Write-Warning ("$logLead : Could not find any https binding for site {0}" -f $_.Name) + } + else { + $hostName = "localhost" + if ($httpBinding.Protocol -eq "https") { + $hostName = $httpBinding.Host + } + + $urlString = "{0}://{1}" -f $httpBinding.Protocol, $hostName, $_.Name + + # Add Site to Cover CUFX / ORBFX / Mobile Auth + $siteArray += @{Name = $_.Name; Url = $urlString; Ipsts = $false; Admin = $false; Client = $true} + $cleanUrl = $urlString.TrimEnd("/") + + foreach ($endpoint in $endpoints){ + $siteArray += @{Name = $_.Name; Url = ($cleanUrl + "/$endpoint"); Ipsts = $false; Admin = $false; Client = $true} + } + + } + } + + $scriptBlock = { + param ($site, $testPattern) + + try { + add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ + + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + $response = Invoke-WebRequest -Uri $site.Url -UseBasicParsing + $stopWatch.Stop() + + if ($response.StatusCode -ne 200 -or $response.Content -notmatch $testPattern) { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $false + StatusCode = $response.StatusCode + Elapsed = $stopWatch.Elapsed.ToString() + } + } + else { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $true + StatusCode = "200" + Elapsed = $stopWatch.Elapsed.ToString() + } + } + } + catch { + return New-Object -TypeName PSObject -Property @{ + Name = $site.Name + URL = $site.Url.ToLowerInvariant() + Success = $false + StatusCode = "Error" + Elapsed = $stopWatch.Elapsed.ToString() + } + } + } + + $siteResults = @() + Write-Host ("$logLead : Starting Site Warmup") + foreach ($site in $siteArray) { + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $site, $testPattern + $running = @($jobs | Where-Object {$_.State -eq 'Running'}) + + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) { + (Wait-Job -Job $jobs -Any) | Out-Null + $running = @($jobs | Where-Object {$_.State -eq 'Running'}) + } + } + + Wait-Job -Job $jobs > $null + + $failed = @($jobs | Where-Object {$_.State -eq 'Failed'}) + if ($failed.Count -gt 0) { + $failed | ForEach-Object { $_.ChildJobs[0].JobStateInfo.Reason.Message } + } + + $jobs | ForEach-Object { + $siteResults += $_ | Receive-Job | Select-Object URL, Success, StatusCode, Elapsed + } + + $functionStopWatch.Stop() + + if ($skipOutput) { + return $siteResults + } + else { + if ($null -ne ($siteResults | Where-Object {$_.Success -eq $false})) { + Write-Warning ("$logLead : One or more URLs failed the test case:`n") + } + + $siteResults | Format-Table -Property @{Label = "URL"; Width = 70; e = {$_.URL}; Alignment = "Left"}, @{Label = "Success"; Width = 15; e = {$_.Success}; Alignment = "Right"}, @{Label = "Elapsed"; Width = 25; e = {$_.Elapsed}; Alignment = "Right"} | Out-String + Write-Output ("$logLead : Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString()) + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Remove-WebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Remove-WebBinding.ps1 new file mode 100644 index 0000000..87055e7 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Remove-WebBinding.ps1 @@ -0,0 +1,48 @@ +function Remove-WebBinding { +<# +.SYNOPSIS + Remove the webbinding on a given site + url + if it's SSL or not. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $true)] + [string]$website, + + [Parameter(Mandatory = $true)] + [string]$url, + + [Parameter(Mandatory = $false)] + [switch]$ssl + ) + + $logLead = (Get-LogLeadName); + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + [string]$hostHeader = [string]::Empty + if ($ssl) { + $hostHeader = ("*:443:{0}" -f $url) + } + else { + $hostHeader = ("*:80:{0}" -f $url) + } + + if ($null -eq $mgr.Sites[$website]) { + Write-Host ("$logLead : Website {0} does not exist" -f $website) + return $false + } + else { + [object] $targetBinding = $null + foreach ($webBinding in $mgr.Sites[$website].Bindings) { + if ($webBinding.bindingInformation -eq $hostHeader) { + Write-Output ("$logLead : Site: {0}" -f $website) + Write-Output ("$logLead : Remove binding: {0}" -f $webBinding) + $targetBinding = $webBinding + } + } + $mgr.Sites[$website].Bindings.Remove($targetBinding) + } + $mgr.CommitChanges() +} + +#Set-Alias -name Create-ClientWebBinding -value New-ClientWebBinding; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Repair-ValidIISPaths.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Repair-ValidIISPaths.ps1 new file mode 100644 index 0000000..5c4e533 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Repair-ValidIISPaths.ps1 @@ -0,0 +1,74 @@ +function Repair-ValidIISPaths { + <# + .SYNOPSIS + Ensure that all the paths in IIS are valid paths + Performs No-Op until 'RemediateVirtualDirectories' switch is supplied + + .PARAMETER RemediateVirtualDirectories + Remediate virtual directories to prevent errors + #> + [CmdletBinding()] + param ( + [switch]$RemediateVirtualDirectories + ) + + $logLead = Get-LogLeadName + + if (-not (Test-Path -Path "IIS:")) { + Write-Host "$logLead : IIS Drive not found, Run [Import-Module WebAdministration]" + } + + # This hard-coded path will never change. We do not need to componentize it or anything into a separate function + $configuration = ([xml](Get-Content -Path 'C:\Windows\System32\inetsrv\config\applicationHost.config')).configuration + + $emptyPath = Join-Path -Path (Get-OrbPath) -ChildPath "Empty" + + # Ensure the path exists if we will use it + if ($RemediateVirtualDirectories) { + if (-not (Test-Path -Path $emptyPath)) { + Write-Host "$logLead : Creating [$emptyPath]" + New-Item -ItemType Directory -Path $emptyPath -Force | Out-Null + } + } + + # Collect the data, because we should probably do a remediation + # For now, alert on this information + $failingApplicationPaths = @() + $failingFailurePaths = @() + + foreach ($site in $configuration.'system.applicationHost'.sites.site) { + foreach ($application in $site.application) { + foreach ($virtualDirectory in $application.virtualdirectory) { + $virtualDirectoryPhysicalPath = $virtualdirectory.physicalpath + if ($virtualDirectoryPhysicalPath -eq "%SystemDrive%\inetpub\wwwroot") { + Write-Verbose "$logLead : Skipping check for [$virtualDirectoryPhysicalPath]" + Continue + } + if (-not (Test-Path -Path $virtualDirectoryPhysicalPath)) { + $failingApplicationPaths += @{ Site = $site.name; ApplicationPool = $application.applicationPool; ApplicationPath = $application.path; Path = $virtualDirectoryPhysicalPath; } + if ($RemediateVirtualDirectories) { + Write-Host "$logLead : Path [$virtualDirectoryPhysicalPath] does not exist for ApplicationPool: [$($application.applicationPool)] at Virtual Dir path: [$($virtualDirectory.path)] under [$($site.name)], setting to [$emptyPath]" + $iisPath = Join-Path -Path "IIS:\Sites\$($site.name)\$($application.path)" -ChildPath $virtualDirectory.path + Set-ItemProperty $iisPath -Name physicalPath -Value $emptyPath + } else { + # TODO: TeamCity error? + Write-Warning "$logLead : Path [$virtualDirectoryPhysicalPath] does not exist for [$($application.applicationPool)|$($virtualDirectory.path)] under [$($site.name)]" + } + } + } + } + } + + foreach ($applicationPool in $configuration.'system.applicationHost'.applicationPools.add) { + $appPoolFailureNodePhysicalPath = $applicationPool.failure.autoShutdownExe + + # Not all nodes have this configured + if (Test-StringIsNullOrWhitespace -Value $appPoolFailureNodePhysicalPath) { continue } + + if (-not (Test-Path -Path $appPoolFailureNodePhysicalPath)) { + Write-Warning "$logLead : Application pool [$($applicationPool.name)] has a configured failure/autoShutdownExe that is missing on disk [$appPoolFailureNodePhysicalPath]" + $failingFailurePaths += @{ ApplicationPool = $applicationPool.name; Path = $appPoolFailureNodePhysicalPath; } + # We should likely take an opportunity to find the sites/site/application that use this applicationPool to point out that it will break as well due to this + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Restart-AlkamiAppPool.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Restart-AlkamiAppPool.ps1 new file mode 100644 index 0000000..f665e21 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Restart-AlkamiAppPool.ps1 @@ -0,0 +1,90 @@ +function Restart-AlkamiAppPool { +<# +.SYNOPSIS + Restarts an application pool and opens the user's default browser to warm it up +#> + + [CmdletBinding()] + Param() + DynamicParam { + # Define the Paramater Attributes + $ParameterName = "AppPoolName" + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ParameterAttribute.Mandatory = $true + $ParameterAttribute.Position = 0 + $AttributeCollection.Add($ParameterAttribute) + + # Generate and add the ValidateSet + $arrSet = $appTierApplications | ForEach-Object {$_.WebAppName} + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + + # Create the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary + } + + begin { + # Bind the parameter to a variable + $AppPoolName = $PsBoundParameters[$ParameterName] + } + + process { + try + { + $logLead = (Get-LogLeadName); + + $mgr = New-Object Microsoft.Web.Administration.ServerManager + $appPool = $mgr.ApplicationPools[$appPoolName] + $processId = $appPool.WorkerProcesses.ProcessId + + Write-Output ("$logLead : Recycling application pool {0}" -f $AppPoolName) + $appPool.Recycle() | Out-Null + + $serviceEndpoint = ($appTierApplications | Where-Object {$_.WebAppName -eq $AppPoolName} | Select-Object -First 1).Endpoint + $site = "http://localhost/$AppPoolName/$serviceEndpoint" + $browserId = Open-UrlInDefaultBrowser $site + + Start-Sleep -Seconds 15 + + try + { + if ($null -ne (Get-Process -Id $processId -ErrorAction SilentlyContinue)) + { + Write-Verbose ("$logLead : Stopping old application pool process ID {0}" -f $processId) + Stop-Process -Id $processId -ErrorAction SilentlyContinue -Force + } + } + catch + { + Write-Warning ("$logLead : An unexpected exception occurred while stopping the old application pool process, which is probably due to the process previously exiting") + } + + try + { + if ($null -ne (Get-Process -Id $browserId -ErrorAction SilentlyContinue)) + { + Write-Verbose ("$logLead : Stopping browser process ID {0}" -f $browserId) + Stop-Process -Id $browserId + } + } + catch + { + ## If this doesn't work the user can close their own browser + Write-Warning "Unable to stop the browser process ID. Manual intervention of browser closing will have to occur." + } + + Write-Output ("$logLead : Completed restart of application pool {0}" -f $AppPoolName) + } + finally + { + if ($null -ne $mgr) + { + $mgr = $null + } + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Save-IISServerManagerChanges.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Save-IISServerManagerChanges.ps1 new file mode 100644 index 0000000..8372167 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Save-IISServerManagerChanges.ps1 @@ -0,0 +1,24 @@ +function Save-IISServerManagerChanges { +<# +.SYNOPSIS + Commits changes from a Microsoft.Web.Administration.ServerManager object + +.DESCRIPTION + Commits changes from a Microsoft.Web.Administration.ServerManager object. Used for mocking + +.PARAMETER serverManager + A Microsoft.Web.Administration.ServerManager object + +.EXAMPLE + $mgr = New-Object Microsoft.Web.Administration.ServerManager + Save-IISServerManagerChanges $mgr + +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [object]$serverManager + ) + + ([Microsoft.Web.Administration.ServerManager]$serverManager.CommitChanges()) | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.ps1 new file mode 100644 index 0000000..1670a42 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.ps1 @@ -0,0 +1,117 @@ +Function Set-AlkamiWebAppPoolConfiguration { +<# +.SYNOPSIS + Create a new web app pool with the Alkami configurations as expected + +.DESCRIPTION + Create a new web app pool with the Alkami configurations as expected + +.OUTPUTS + Returns the application pool that was created + +.PARAMETER AppPoolName + [string] The name of the web application pool + +.EXAMPLE + Set-AlkamiWebAppPoolConfiguration "cole23423444444" + +Note the return at the end of the method for the object itself + +PS Z:\> Set-AlkamiWebAppPoolConfiguration "cole23423444444" +nothing to configure for cole23423444444 - /autoStart:true +nothing to configure for cole23423444444 - /enable32BitAppOnWin64:false +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /managedRuntimeVersion:'v4.0' +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /queueLength:"5000" +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /startMode:"AlwaysRunning" +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /processModel.idleTimeout:"00:00:00" +nothing to configure for cole23423444444 - /processModel.loadUserProfile:true +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /failure.rapidFailProtectionInterval:"00:10:00" +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /failure.rapidFailProtectionMaxCrashes:50 +[Set-AlkamiWebAppPoolConfiguration] : Setting AppPool cole23423444444 Config to /recycling.periodicRestart.time:"00:00:00" +nothing to configure for cole23423444444 - /recycling.LogEventOnRecycle:"Time, Requests, Schedule, Memory, IsapiUnhealthy, OnDemand, ConfigChange, PrivateMemory" + +Name State .NET Pipeline Identity +---- ----- ---- -------- -------- +cole23423444444 Started 'v4.0' Integrated ApplicationPoolIdentity + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [Alias("Name")] + [string]$AppPoolName + ) + + $logLead = (Get-LogLeadName) + + $appPool = IISAdministration\Get-IISAppPool -Name $AppPoolName + + if ($null -eq $appPool) { + $appPool = WebAdministration\New-WebAppPool -Name $AppPoolName -Force + } + + $propertiesToCheck = @( + "/autoStart:true" + "/enable32BitAppOnWin64:false" + "/managedRuntimeVersion:v4.0" + "/queueLength:`"5000`"" + "/startMode:`"AlwaysRunning`"" + "/processModel.idleTimeout:`"00:00:00`"" + "/processModel.loadUserProfile:true" + "/failure.rapidFailProtectionInterval:`"00:10:00`"" + "/failure.rapidFailProtectionMaxCrashes:50" + "/recycling.periodicRestart.time:`"00:00:00`"" + "/recycling.LogEventOnRecycle:`"Time, Requests, Schedule, Memory, IsapiUnhealthy, OnDemand, ConfigChange, PrivateMemory`"" + ) + + $changedProperties = 0 + + ## by using the for-loop we can easily add a single new property to check/set + foreach ($property in $propertiesToCheck) { + + ## This command lists all the apppools that have this property set (since it varies by property, we can't cache it) + ## So we filter the results and look for the matching app pool name to be present in the output + + ## TODO: cbrand ~ This is a terrible line to read. Fix this crap + ## What this command does is + ## 1) negate the results + ## 2) ask appCmdPath to list the application pools with the specified property + ## which gives all of the app pools that match + ## 3) filter to the app pool we care about + ## So we negate the list because if we came back with no records after the filter, + ## then the record we want doesn't exist, so we should set it. + if (!(Test-AppCommandPropertyExistsOnAppPool -Property $property -AppPoolName $AppPoolName)) { + Write-Verbose "$logLead : Setting AppPool $AppPoolName Config to $property" + + Set-AppCommandPropertyOnAppPool -Property $property -AppPoolName $AppPoolName + + $changedProperties += 1 + } else { + Write-Verbose "Nothing to configure for $AppPoolName - $property" + } + } + + Write-Host "$logLead : Checking to attempt to add User from the configuration settings on this host" + if (![string]::IsNullOrWhiteSpace((Get-AppSetting "Environment.UserPrefix" -SuppressWarnings))) { + $appServiceName = (Get-AppServiceAccountName $AppPoolName) + Write-Host "$logLead : Attempting to add User from the configuration settings on [$AppPoolName] with [$appServiceName]" + if ((![string]::IsNullOrEmpty($appServiceName)) -and ($appPool.ProcessModel.UserName -ne $appServiceName)) { + Write-Verbose "$logLead : Setting ExecutionUser for [$AppPoolName] to [$appServiceName]" + Set-ItemProperty $appPoolPath -name processModel -value @{userName=$appServiceName;identitytype=3} + ## Presume that all accounts are currently on Windows and are gMSA, so no password is needed + } else { + if ([string]::IsNullOrEmpty($appServiceName)) { + Write-Host "$logLead : Could not add a specific user to the app pool as the lookup service name does not exist. This is normal and expected behavior, just informational." + } else { + Write-Host "$logLead : No need to change the user as the username is already set correctly." + } + } + } + + return (IISAdministration\Get-IISAppPool -Name $AppPoolName) +} + +## TODO: Review all usages of these aliases in the future so we aren't double-doing the work here, since it does it all every time. +## Alternately: separate things into separate functions and ensure these are always called whenever Set-AlkamiWebAppPoolConfiguration is called. +Set-Alias -name Get-AlkamiWebAppPool -value Set-AlkamiWebAppPoolConfiguration +Set-Alias -name New-AlkamiWebAppPool -value Set-AlkamiWebAppPoolConfiguration \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.tests.ps1 new file mode 100644 index 0000000..1b590d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AlkamiWebAppPoolConfiguration.tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-AlkamiWebAppPoolConfiguration" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-AppServiceAccountName -MockWith { "this string is not empty" } + Mock -ModuleName $moduleForMock -CommandName Set-ItemProperty -MockWith { } + Mock -ModuleName $moduleForMock -CommandName WebAdministration\New-WebAppPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-Item -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Test-AppCommandPropertyExistsOnAppPool -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Set-AppCommandPropertyOnAppPool -MockWith { } + Mock -ModuleName $moduleForMock -CommandName IISAdministration\Get-IISAppPool -MockWith { New-Object PSObject } + Mock -ModuleName $moduleForMock -CommandName Get-AppSetting -MockWith { return "" } + + $appPoolName = "Totally fake app name" + + Context "Integration Tests" { + It "Does nothing if all results are 'already found'" { + Mock -ModuleName $moduleForMock -CommandName Test-AppCommandPropertyExistsOnAppPool -MockWith { $true } + + Set-AlkamiWebAppPoolConfiguration -AppPoolName $appPoolName + + Assert-MockCalled -CommandName Test-AppCommandPropertyExistsOnAppPool -Times 11 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Set-AppCommandPropertyOnAppPool -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName IISAdministration\Get-IISAppPool -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Does all actions if no results are 'already found'" { + Mock -ModuleName $moduleForMock -CommandName Test-AppCommandPropertyExistsOnAppPool -MockWith { $false } + + Set-AlkamiWebAppPoolConfiguration -AppPoolName $appPoolName + + Assert-MockCalled -CommandName Test-AppCommandPropertyExistsOnAppPool -Times 11 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Set-AppCommandPropertyOnAppPool -Times 11 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName IISAdministration\Get-IISAppPool -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AllAppPoolDefaults.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AllAppPoolDefaults.ps1 new file mode 100644 index 0000000..12b6532 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AllAppPoolDefaults.ps1 @@ -0,0 +1,23 @@ +function Set-AllAppPoolDefaults { +<# +.SYNOPSIS + Sets all Application Pools to the Alkami Defaults +#> + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + $appPoolsToCheck = (Get-ChildItem IIS:\AppPools\).Where({$_.Name -notlike ".NET *" -and $_.ManagedRuntimeVersion -eq "v4.0"}) + Write-Verbose ("$logLead : Found {0} application pools to check" -f $appPoolsToCheck.Count) + + foreach ($appPool in $appPoolsToCheck) { + (Set-AlkamiWebAppPoolConfiguration $appPool.Name) + } + + $sites = (Get-ChildItem IIS:\Sites\) + foreach ($site in $sites) { + (Set-ItemProperty "IIS:\Sites\$($site.Name)" -Name applicationDefaults.preloadEnabled -Value $true) | Out-Null + } +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AppCommandPropertyOnAppPool.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AppCommandPropertyOnAppPool.ps1 new file mode 100644 index 0000000..4a92d05 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AppCommandPropertyOnAppPool.ps1 @@ -0,0 +1,29 @@ +Function Set-AppCommandPropertyOnAppPool { +<# +.SYNOPSIS + Set the property on an app pool using the appcmd.exe tool + +.PARAMETER Property + [string] The property to set + +.PARAMETER AppPoolName + [string] The name of the web application pool +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true, Position=0)] + [Alias("Name")] + [string]$Property, + + [Parameter(Mandatory=$true, Position=1)] + [string]$AppPoolName +) + $logLead = (Get-LogLeadName) + + ## TODO: cbrand ~ Replace with a call to a function to find this in the right place + $appCmdPath = (Join-Path $env:systemroot "\system32\inetsrv\appcmd.exe") + + Write-Verbose "$logLead : Running AppCmd for [$appCmdPath set apppool $AppPoolName $property]" + + (& $appCmdPath set apppool $AppPoolName $Property) | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.ps1 new file mode 100644 index 0000000..01d38e0 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.ps1 @@ -0,0 +1,23 @@ +function Set-AppTierDefaultWebSite { +<# +.SYNOPSIS + Ensure there is a default website and it is properly configured to be how we expect the defaults to be configured +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [string]$defaultWebSiteName = "Default Web Site" + ) + + $logLead = (Get-LogLeadName) + + if ($null -eq (Get-DefaultWebsite $defaultWebSiteName)) { + Write-Host "$logLead : Adding Default Web Site with name [$defaultWebSiteName]" + + (New-DefaultWebsite $defaultWebSiteName) | Out-Null + } + + Optimize-DefaultWebsite -WebsiteName $defaultWebsiteName +} + +Set-Alias -name Configure-AppTierDefaultWebSite -value Set-AppTierDefaultWebSite; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.tests.ps1 new file mode 100644 index 0000000..94babf7 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierDefaultWebSite.tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-AppTierDefaultWebSite" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-DefaultWebsite -MockWith { return @{ not = "null";} } + Mock -ModuleName $moduleForMock -CommandName New-DefaultWebsite -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Optimize-DefaultWebsite -MockWith { } + + Context "Website exists" { + Mock -ModuleName $moduleForMock -CommandName Get-DefaultWebsite -MockWith { return @{ not = "null";} } + + It "Did not throw and Did not call New-DefaultWebsite" { + { Set-AppTierDefaultWebSite } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-DefaultWebsite -Scope It -Times 0 + } + } + + Context "Website does not exist" { + Mock -ModuleName $moduleForMock -CommandName Get-DefaultWebsite -MockWith { return $null } + + It "Did not throw and Did call New-DefaultWebsite" { + { Set-AppTierDefaultWebSite } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName New-DefaultWebsite -Scope It -Times 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierFolderAndFilePermissions.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierFolderAndFilePermissions.ps1 new file mode 100644 index 0000000..8597783 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-AppTierFolderAndFilePermissions.ps1 @@ -0,0 +1,39 @@ +function Set-AppTierFolderAndFilePermissions { +<# +.SYNOPSIS + This function sets folder and file permissions on the App Tier for App Tier Services, Hosts File, Log Files, etc +#> + + [CmdletBinding()] + Param() + $logLead = (Get-LogLeadName); + + $modifyRight = [System.Security.AccessControl.FileSystemRights]::Modify + $fullControlRight = [System.Security.AccessControl.FileSystemRights]::FullControl + + if (!(Test-Path $logsPath)) { + Write-Output ("$logLead : Could not find log path {0}. Creating it." -f $logsPath) + [System.IO.Directory]::CreateDirectory($logsPath) | Out-Null + } + + Write-Output ("$logLead : Setting Rights Users : Modify" -f $logsPath) + Grant-RightsToFolderOrFile -account "BUILTIN\Users" -path $logsPath -rights $modifyRight + + $hostsFile = "C:\Windows\System32\Drivers\etc\hosts" + Write-Output ("$logLead : Setting Rights Users : Modify" -f $hostsFile) + Grant-RightsToFolderOrFile -account "BUILTIN\Users" -path $hostsFile -rights $modifyRight + + $usersToGrantRightsFor = @() + $usersToGrantRightsFor += "BUILTIN\IIS_IUSRS" + (Get-AppTierServices) | Where-Object {$_.User -ne "REPLACEME"} | ForEach-Object {$_.User} | Sort-Object | Get-Unique -AsString | ForEach-Object { $usersToGrantRightsFor += $_ } + + $usersToGrantRightsFor | Sort-Object | Get-Unique | Where-Object {(!([String]::IsNullOrEmpty($_)))} | ForEach-Object { + + Write-Output ("$logLead : Setting Rights for {1} : Modify" -f $basePath, $_.ToString()) + Grant-RightsToFolderOrFile -account $_ -path $basePath -rights $modifyRight + } + + Write-Output ("$logLead : Setting Rights Administrators : FullControl" -f $logsPath) + Grant-RightsToFolderOrFile -account "BUILTIN\Administrators" -path $basePath -rights $fullControlRight +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-ApplicationPoolExecutionAccount.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-ApplicationPoolExecutionAccount.ps1 new file mode 100644 index 0000000..66cdb3d --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-ApplicationPoolExecutionAccount.ps1 @@ -0,0 +1,39 @@ +function Set-ApplicationPoolExecutionAccount { +<# +.SYNOPSIS + Sets the Execution Account for an Application Pool + +.PARAMETER AppPool + [string] The web application pool. + +.PARAMETER Credential + [PSCredential] The credentials to use for configuration here + +.PARAMETER IsGMSAAccount + [Switch] Is the account credential a GMSAAccount +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [Microsoft.Web.Administration.ApplicationPool]$appPool, + + [Parameter(Mandatory=$true, Position=1)] + [PSCredential]$Credential, + + [Parameter(Mandatory=$false, Position=2)] + [switch]$IsGMSAAccount + ) + + $logLead = (Get-LogLeadName) + + $appPool.ProcessModel.IdentityType = [Microsoft.Web.Administration.ProcessModelIdentityType]::SpecificUser + + Write-Verbose "$logLead : Setting ExecutionUser (GMSA: $IsGMSAAccount) to @($Credential.UserName)" + $appPool.ProcessModel.UserName = $Credential.UserName + + if (!$IsGMSAAccount) { + Write-Verbose "$logLead : Setting Password on $($appPool.Name)" + $appPool.ProcessModel.Password = (Get-PasswordFromCredential $Credential) + } +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-MimeTypeValue.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-MimeTypeValue.ps1 new file mode 100644 index 0000000..8eb0ec1 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-MimeTypeValue.ps1 @@ -0,0 +1,31 @@ +function Set-MimeTypeValue { +<# +.SYNOPSIS + This function sets the MIME type label for a type of data +#> + + [CmdletBinding()] + Param( + [Microsoft.Web.Administration.ConfigurationElementCollection]$collection, + [string]$extension, + [String]$mimeType + ) + + $logLead = (Get-LogLeadName); + $mimeMatch = ($collection | Where-Object {$_.ElementTagName -eq "mimeMap"} | Select-Object -ExpandProperty "RawAttributes" | Where-Object {$_.fileExtension -eq $extension}) + Write-Output ("$logLead : Checking Mimetype {0}" -f $extension) + if ($null -eq $mimeMatch) { + Write-Output ("$logLead : Adding {0} MimeType with value {1}" -f $extension, $mimeType) + $addElement = $collection.CreateElement("mimeMap") + $addElement["fileExtension"] = $extension + $addElement["mimeType"] = $mimeType + + $collection.Add($addElement) | Out-Null + } + elseif ($mimeMatch.mimeType -ne $mimeType) { + Write-Output ("$logLead : Updating MimeType {0} from {1} to {2}" -f $extension, $mimeMatch.mimeType, $mimeType) + $mimeTypeSection = ($collection | Where-Object {$_.Attributes["fileExtension"].Value -eq $extension}) + $mimeTypeSection.Attributes["mimeType"].Value = $mimeType + } +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-ServerMIMETypes.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerMIMETypes.ps1 new file mode 100644 index 0000000..9f0e51d --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerMIMETypes.ps1 @@ -0,0 +1,22 @@ +function Set-ServerMIMETypes { +<# +.SYNOPSIS + This function sets the server's MIME types correctly +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + $config = $mgr.GetWebConfiguration("/") + Write-Verbose ("$logLead : Getting static content configuration") + $contentSectionCollection = $config.GetSection("system.webServer/staticContent").GetCollection() + + Set-MimeTypeValue -collection $contentSectionCollection -extension ".woff" -mimeType "application/font-woff" + Set-MimeTypeValue -collection $contentSectionCollection -extension ".woff2" -mimeType "application/font-woff2" + Set-MimeTypeValue -collection $contentSectionCollection -extension ".webmanifest" -mimeType "application/manifest+json" + + $mgr.CommitChanges() +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.Tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.Tests.ps1 new file mode 100644 index 0000000..e4d8b2f --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.Tests.ps1 @@ -0,0 +1,48 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-ServerResponseHeaders" { + + $canRunTests = $false + + try { + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName + + if (Test-IsAdmin) { + $canRunTests = $true + } else { + Write-Warning "Process Not Running as Admin. Integration Tests Will Not Be Executed" + $canRunTests = $false + } + } catch { + # If IIS Isn't Installed, We Can't Actually Test This in IIS. Only Unit tests will run + Write-Warning "Unable to Load Microsoft.Web.Administration. Integration Tests Will Not Be Executed" + $canRunTests = $false + } + + Context "Integration Tests" { + + Mock -ModuleName $moduleForMock -CommandName Get-IISServerManager -MockWith { + + $global:testManager = New-Object Microsoft.Web.Administration.ServerManager + return $global:testManager + } + + Mock -ModuleName $moduleForMock -CommandName Save-IISServerManagerChanges -MockWith {} + + ## TODO: cbrand ~ Give more test cases to this function + + It "[Integration] Calls Get/Save IISServerManager functions" -Skip:(!$canRunTests) { + + Set-ServerResponseHeaders + + Assert-MockCalled -CommandName Get-IISServerManager -Scope It -ModuleName $moduleForMock -Times 1 -Exactly + Assert-MockCalled -CommandName Save-IISServerManagerChanges -Times 1 -Scope It -Exactly -ModuleName $moduleForMock + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.ps1 new file mode 100644 index 0000000..17ee375 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-ServerResponseHeaders.ps1 @@ -0,0 +1,51 @@ +function Set-ServerResponseHeaders { +<# +.SYNOPSIS + This function sets the server's reponse headers correctly +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + + # Set Server Response Headers + $mgr = Get-IISServerManager + $config = $mgr.GetWebConfiguration("/") + $httpProtocolSection = $config.GetSection("system.webServer/httpProtocol") + $customHeadersCollection = $httpProtocolSection.GetCollection("customHeaders") + + $serverReponseHeader = @() + + foreach($customHeader in $customHeadersCollection) { + if ($customHeader.RawAttributes.value -match "ASP.NET") { + Write-Output ("$logLead : Detected X-Powered-By response header. Removing it.") + $customHeader.Delete() + } + if ($customHeader.RawAttributes.name -match "X-SVR") { + $serverReponseHeader += $customHeader + } + } + + ## If no X-SVR element found, go ahead and create one with the local machine name + if (Test-IsCollectionNullOrEmpty $serverReponseHeader) { + Write-Output ("$logLead : X-SVR response header not found. Creating it.") + if ($env:COMPUTERNAME -like 'alka*') { + # Legacy naming convention + [Regex]$regex = "(\w{1}\d+$)" + $serverResponseValue = ($regex.Matches($env:COMPUTERNAME).Groups | Select-Object -First 1).Value + } + else { + # Use entire machine name + $serverResponseValue = $env:COMPUTERNAME + } + + $addElement = $customHeadersCollection.CreateElement("add") + $addElement["name"] = "X-SVR" + $addElement["value"] = $serverResponseValue + + $customHeadersCollection.Add($addElement) | Out-Null + } + + Save-IISServerManagerChanges $mgr +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierDefaultWebSite.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierDefaultWebSite.ps1 new file mode 100644 index 0000000..9f9053a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierDefaultWebSite.ps1 @@ -0,0 +1,43 @@ +function Set-WebTierDefaultWebSite { +<# +.SYNOPSIS + Ensure there is a default website and it is properly configured to be how we expect the defaults to be configured +#> + + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName); + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + if ($null -eq $mgr.Sites["Default Web Site"]) { + Write-Output "$logLead : Adding Default Web Site" + $mgr.Sites.Add("Default Web Site", "C:\Inetpub\wwwroot", "80") | Out-Null + } + + $site = $mgr.Sites["Default Web Site"] + $sslBinding = $site.Bindings | Where-Object {$_.Protocol -eq "https"} + + if ($null -eq $sslBinding) { + Write-Output "$logLead : SSL binding for Default Web Site not found -- creating it" + + $sslBindingText = "*:443:" + $personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My + $certificate = @(Get-ChildItem cert:\localmachine\my | Where-Object { $_.FriendlyName -match "WMSVC" })[0] + + if ($null -eq $certificate) { + Write-Warning ("$logLead : Could not locate WMSVC certificate to bind to the Default Web Site. Create the SSL binding manually or NLB health checks may fail!") + return + } + + ($site.Bindings.Add($sslBindingText, $certificate.GetCertHash(), $personalStore, [Microsoft.Web.Administration.SslFlags]::None)) | Out-Null + $mgr.CommitChanges() + } + elseif ($sslBinding.SslFlags.HasFlag([Microsoft.Web.Administration.SslFlags]::Sni)) { + Write-Output "$logLead : SSL binding for Default Web Site has the Sni flag -- clearing it" + $sslBinding.SslFlags = [Microsoft.Web.Administration.SslFlags]::None; + $mgr.CommitChanges() + } +} + +Set-Alias -name Configure-WebTierDefaultWebSite -value Set-WebTierDefaultWebSite; \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierFolderAndFilePermissions.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierFolderAndFilePermissions.ps1 new file mode 100644 index 0000000..ce9a048 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Set-WebTierFolderAndFilePermissions.ps1 @@ -0,0 +1,33 @@ +function Set-WebTierFolderAndFilePermissions { +<# +.SYNOPSIS + This function sets folder and file permissions on the Web Tier for Services, Hosts File, Log Files, etc +#> + + [CmdletBinding()] + Param() + $logLead = (Get-LogLeadName); + + $modifyRight = [System.Security.AccessControl.FileSystemRights]::Modify + $fullControlRight = [System.Security.AccessControl.FileSystemRights]::FullControl + + if (!(Test-Path $logsPath)) { + Write-Output ("$logLead : Could not find log path {0}. Creating it." -f $logsPath) + [System.IO.Directory]::CreateDirectory($logsPath) | Out-Null + } + + Write-Output ("$logLead : Setting Rights Users : Modify" -f $logsPath) + Grant-RightsToFolderOrFile -account "BUILTIN\Users" -path $logsPath -rights $modifyRight + + $hostsFile = "C:\Windows\System32\Drivers\etc\hosts" + Write-Output ("$logLead : Setting Rights Users : Modify" -f $hostsFile) + Grant-RightsToFolderOrFile -account "BUILTIN\Users" -path $hostsFile -rights $modifyRight + + $iisUsers = "BUILTIN\IIS_IUSRS" + Write-Output ("$logLead : Setting Rights for {1} : Modify" -f $basePath, $iisUsers) + Grant-RightsToFolderOrFile -account $iisUsers -path $basePath -rights $modifyRight + + Write-Output ("$logLead : Setting Rights Administrators : FullControl" -f $logsPath) + Grant-RightsToFolderOrFile -account "BUILTIN\Administrators" -path $basePath -rights $fullControlRight +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.ps1 new file mode 100644 index 0000000..3d236f6 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.ps1 @@ -0,0 +1,46 @@ +function Start-IISAndServices { + + <# + .SYNOPSIS + Starts both IIS and Alkami Windows Services + + .DESCRIPTION + Starts both IIS and Alkami Windows Services. Max service start parallelism defaults to 10 and can be set by specifying a positive int for the parameter maxParallel. + IIS is started first. Dependent windows services are started next serially, via Start-DependentServices. Remaining Alkami services are started last + + .PARAMETER maxParallel + [int] The maximum number of services to start in parallel. Defaults to 10 + + .EXAMPLE + Start-IISAndServices -maxParallel 2 + +[Start-IISAndServices] : Service start parallelism set to 2 +[Start-IISOnly] : Starting IIS... +[Start-IISOnly] : Done +[Start-IISAndServices] : Sleeping for 5 seconds... +[Start-DependentServices] : No Dependent Services Found to Start +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 3 chocolatey services. +[Start-IISAndServices] : No Services Found to Start + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$maxParallel = 10 + ) + + $logLead = Get-LogLeadName + if ($maxParallel -ne 10) { + + Write-Host "$logLead : Service start parallelism set to $maxParallel" + } + + Start-IISOnly + + Write-Host "$logLead : Sleeping for 5 seconds..." + Start-Sleep -Seconds 5 + + Start-ServicesOnly -maxParallel $maxParallel +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.tests.ps1 new file mode 100644 index 0000000..53c42d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAndServices.tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-IISAndServices" { + + Context "Parameter Validation" { + + It "Uses the Supplied Max Parallel Parameter" { + + Mock -ModuleName $moduleForMock -CommandName Start-Sleep -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Start-ServicesOnly -MockWith { } + + $maxParallelTestValue = 20 + Start-IISAndServices -maxParallel $maxParallelTestValue + Assert-MockCalled -CommandName Start-ServicesOnly -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $maxParallel -eq $maxParallelTestValue } + } + + It "Writes to Host if a Non-Default Max Parallel Param is Used" { + + Mock -ModuleName $moduleForMock -CommandName Start-Sleep -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Start-ServicesOnly -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + + $maxParallelTestValue = 20 + Start-IISAndServices -maxParallel $maxParallelTestValue + Assert-MockCalled -CommandName Write-Host -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Object -match "Service start parallelism set to $maxParallelTestValue" } + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.ps1 new file mode 100644 index 0000000..70b6db5 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.ps1 @@ -0,0 +1,24 @@ +function Start-IISAppPoolByName { +<# +.SYNOPSIS + Starts a given IIS App Pool by name +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("AppPoolName")] + [string]$Name = "" + ) + process { + $logLead = (Get-LogLeadName) + + Write-Verbose "$logLead : Attempting to Start app pool by name" + + # Test-IISAppPoolByName returns false if the process is not running. We want to start it if it is not running. Negate the response. + if (!(Test-IISAppPoolByName $name)) { + Start-WebAppPool -Name $name + } + + Write-Verbose "$logLead : Done" + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.tests.ps1 new file mode 100644 index 0000000..a6c0276 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Start-IISAppPoolByName.tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-IISAppPoolByName" { + $appPoolName = "FakeAppPool" + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Start-WebAppPool -MockWith { } + + Context "App Exists and Is Not Running" { + Mock -ModuleName $moduleForMock -CommandName Test-IISAppPoolByName -MockWith { return $false } + + It "Does not throw and Calls Start-WebAppPool once" { + { Start-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Start-WebAppPool -Scope It -Times 1 + } + } + + Context "App Exists and Is Running" { + Mock -ModuleName $moduleForMock -CommandName Test-IISAppPoolByName -MockWith { return $true } + + It "Does not throw and Does not Call Start-WebAppPool" { + { Start-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Start-WebAppPool -Scope It -Times 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Start-IISOnly.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Start-IISOnly.ps1 new file mode 100644 index 0000000..df6b46f --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Start-IISOnly.ps1 @@ -0,0 +1,20 @@ +function Start-IISOnly { +<# +.SYNOPSIS + Starts IIS by running iisreset /start +#> + [CmdletBinding()] + [OutputType([void])] + Param( + + ) + + $logLead = Get-LogLeadName + + Write-Host "$logLead : Starting IIS..." + + Invoke-Command { C:\Windows\System32\iisreset.exe /start } | Out-Null + + Write-Host "$logLead : Done" +} + diff --git a/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAndServices.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAndServices.ps1 new file mode 100644 index 0000000..b0d88cb --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAndServices.ps1 @@ -0,0 +1,49 @@ +function Stop-IISAndServices { +<# +.SYNOPSIS + Stops both IIS and Windows Services + +.PARAMETER ExclusionList + [[-ExclusionList] ] Array of service names to exclude from being stopped +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string[]]$exclusionList, + [Parameter(Mandatory = $false)] + [switch]$FeaturePassAllSkipSwitchesToGetServicesToStop + ) + + $logLead = (Get-LogLeadName) + + Stop-IISOnly + + Write-Host "$logLead : Sleeping for 5 seconds..." + Start-Sleep -Seconds 5 + + # We use this to pass-thru switches elsewhere. It's not pretty, but it avoids ugly if-else blocks + # and does the same thing, now that "if($switchParam)" is the same as "if($switchParam.IsPresent" + # and both work whether you don't pass them or pass $true/$false. + # + # This will - as of 2021-11-04 - only give us the retval of Get-FileBeatsService + # while still allowing us to Stop-IisOnly AND SMSvcHost AND RazorCore processes + $services = Get-ServicesToStop ` + -skipSubscriptionService:$FeaturePassAllSkipSwitchesToGetServicesToStop ` + -skipChocolateyServices:$FeaturePassAllSkipSwitchesToGetServicesToStop ` + -skipAlkamiServices:$FeaturePassAllSkipSwitchesToGetServicesToStop + + if (!(Test-IsCollectionNullOrEmpty -Collection $exclusionList)) { + Write-Host -Object "$logLead : Number of services to exclude : $($exclusionList.Count)" + $services = $services | Where-Object { !$exclusionList.Contains($_) } + } + + if ($services) { + Write-Host -Object "$logLead : Number of services to stop in parallel : $($services.count)" + Stop-ServicesInParallel -ServiceNamestoStop $services + } + + Stop-ProcessIfFound -ProcessName "SMSvcHost" + Stop-ProcessIfFound -ProcessName "Alkami.MicroServices.Notifications.Service.RazorCore" + # SRE-18110 - Remove this line after DEV-139783 is worked. + Stop-ProcessIfFound -ProcessName "Alkami.App.Providers.CheckOrders.Harland.CryptlibWrapper.exe" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.ps1 new file mode 100644 index 0000000..89c0d32 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.ps1 @@ -0,0 +1,24 @@ +function Stop-IISAppPoolByName { +<# +.SYNOPSIS + Stops a given IIS App Pool by name +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "We literally want to stop it for automation, of course we are gonna change system state.")] + [CmdletBinding(ConfirmImpact = 'None')] + Param( + [Parameter(Mandatory=$false)] + [Alias("AppPoolName")] + [string]$Name = "" + ) + process { + $logLead = (Get-LogLeadName) + + Write-Verbose "$logLead : Attempting to stop app pool by name" + + if (Test-IISAppPoolByName $Name) { + Stop-WebAppPool -Name $name + } + + Write-Verbose "$logLead : Done" + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.tests.ps1 new file mode 100644 index 0000000..a6fdefe --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISAppPoolByName.tests.ps1 @@ -0,0 +1,38 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Stop-IISAppPoolByName" { + $appPoolName = "FakeAppPool" + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Stop-WebAppPool -MockWith { } + + Context "App Exists and Is Running" { + Mock -ModuleName $moduleForMock -CommandName Test-IISAppPoolByName -MockWith { return $true } + + + It "Does not throw and Calls Stop-WebAppPool once" { + { Stop-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-WebAppPool -Scope It -Times 1 + } + } + + Context "App Exists and Is Not Running" { + Mock -ModuleName $moduleForMock -CommandName Test-IISAppPoolByName -MockWith { return $false } + + + It "Does not throw and Does not Call Stop-WebAppPool" { + { Stop-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-WebAppPool -Scope It -Times 0 + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Stop-IISOnly.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISOnly.ps1 new file mode 100644 index 0000000..36da6a3 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Stop-IISOnly.ps1 @@ -0,0 +1,17 @@ +function Stop-IISOnly { +<# +.SYNOPSIS + Stops IIS by running iisreset /stop +#> + [CmdletBinding()] + param( + ) + + $logLead = (Get-LogLeadName); + + Write-Output "$logLead : Stopping IIS..."; + + Invoke-Command { C:\Windows\System32\iisreset.exe /stop } | Out-Null; + + Write-Output "$logLead : Done"; +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-AppCommandPropertyExistsOnAppPool.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-AppCommandPropertyExistsOnAppPool.ps1 new file mode 100644 index 0000000..ea9e9d0 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-AppCommandPropertyExistsOnAppPool.ps1 @@ -0,0 +1,35 @@ +Function Test-AppCommandPropertyExistsOnAppPool { +<# +.SYNOPSIS + Set the property on an app pool using the appcmd.exe tool + +.PARAMETER Property + [string] The property to set + +.PARAMETER AppPoolName + [string] The name of the web application pool +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true, Position=0)] + [Alias("Name")] + [string]$Property, + + [Parameter(Mandatory=$true, Position=1)] + [string]$AppPoolName +) + ## TODO: cbrand ~ Replace with a call to a function to find this in the right place + $appCmdPath = (Join-Path $env:systemroot "\system32\inetsrv\appcmd.exe") + + $returnValue = $false + + $propertiesFoundOn = @(& $appCmdPath list apppool $Property) + + foreach($PropertyReturned in $propertiesFoundOn) { + if ($PropertyReturned -match $AppPoolName) { + $returnValue = $false + } + } + + return $returnValue +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.ps1 new file mode 100644 index 0000000..d21a5b6 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.ps1 @@ -0,0 +1,48 @@ +function Test-IISAppPoolByName { +<# +.SYNOPSIS + Tests IIS to see if a specific app pool is running + Will throw if there is no app pool +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory=$false)] + [Alias("AppPoolName")] + [string]$Name = "" + ) + process { + $logLead = (Get-LogLeadName) + + Write-Verbose "$logLead : Finding app pool by name" + + if (!$Name) { + Write-Host "The following are the valid names for application pools:" + Write-Host (((IISAdministration\Get-IISAppPool).Name) -Join "`r`n") + # This should halt, returning false could be construed as the app pool exists + throw "$logLead : $Name was empty or not supplied" + } else { + + try { + $appPool = IISAdministration\Get-IISAppPool -Name $name -ErrorAction Ignore + if ($null -eq $appPool) { + # This should halt, returning false could be construed as the app pool exists + # Setting to return false tho because of some sites being configured not-as-expected + Write-Warning "$logLead : $Name is not a valid AppPool name" + return $false; + } + else + { + return $appPool.State -eq "Started" + } + } catch { + # This should halt, returning false could be construed as the app pool exists + # Setting to return false tho because of some sites being configured not-as-expected + Write-Warning "$logLead : $Name is not a valid AppPool name" + return $false; + } + } + + return $false + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.tests.ps1 new file mode 100644 index 0000000..2e36008 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-IISAppPoolByName.tests.ps1 @@ -0,0 +1,49 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-IISAppPoolByName" { + $appPoolName = "FakeAppPool" + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + Mock -ModuleName $moduleForMock -CommandName Write-Verbose -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { } + + Context "No name provided" { + Mock -ModuleName $moduleForMock -CommandName IISAdministration\Get-IISAppPool -MockWith { return @( @{ Name = $appPoolName;} ) } + It "Name Is Empty Should Throw" { + { Test-IISAppPoolByName -Name "" } | Should -Throw + } + } + + <# + # Commenting this out because we have some pools that break things in prod but don't exist in lower environments + Context "Name is not valid" { + Mock -ModuleName $moduleForMock -CommandName IISAdministration\Get-IISAppPool -MockWith { return $null } + + { Test-IISAppPoolByName -Name $appPoolName } | Should -Throw + } +#> + + Context "Name is valid, Service is started" { + Mock -ModuleName $moduleForMock -CommandName IISAdministration\Get-IISAppPool -MockWith { return @{ Name = $appPoolName; Status = 'Started'; } } + + It "Has Valid Name And Is Started Should Not Throw" { + { Test-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + } + } + + Context "Name is valid, Service is not started" { + Mock -ModuleName $moduleForMock -CommandName IISAdministration\Get-IISAppPool -MockWith { return @{ Name = $appPoolName; Status = 'Stopped'; } } + + It "Has Valid Name And Is Not Started Should Not Throw" { + { Test-IISAppPoolByName -Name $appPoolName } | Should -Not -Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-InstallerUseSymlinkStrategy.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-InstallerUseSymlinkStrategy.ps1 new file mode 100644 index 0000000..35d3675 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-InstallerUseSymlinkStrategy.ps1 @@ -0,0 +1,30 @@ +Function Test-InstallerUseSymlinkStrategy { +<# +.SYNOPSIS + Does the new installer use the symlink strategy? Set the value with [System.Environment]::SetEnvironmentVariable("Alkami.Installer.UseSymlink","true", "Machine") + +.DESCRIPTION + This tests an environment variable and returns a boolean if the variable is set. + +.OUTPUTS + [bool] if the machine is using Symlink strategy. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + process { + $loglead = Get-LogLeadName + + $useSymlinkValue = Get-EnvironmentVariable -Name "Alkami.Installer.UseSymlink" + + $useSymlink = $false + if( -not ([string]::IsNullOrWhiteSpace($useSymlinkValue)) -and -not ([bool]::TryParse($useSymlinkValue, [ref]$useSymlink))) { + Write-Warning "$logLead : Could not properly parse the value of the env variable [Alkami.Installer.UseSymlink] got [$useSymlinkValue]" + } + + Write-Host "$logLead : Do we use the new installer pattern? [$useSymlink]" + + return $useSymlink + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.ps1 new file mode 100644 index 0000000..3bc2c30 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.ps1 @@ -0,0 +1,54 @@ +function Test-KnownWCFServicesResolvable { +<# +.SYNOPSIS + Test that a set of WCF services are resolvable + +.PARAMETER Services + This should be an array of one or more services with a property set minimum of: + * Name + * WebAppName + * Endpoint + + The Name is assumed to be the value used as the hostname for purposes of service resolution for a given WebAppName. + +.PARAMETER KnownSkipWebApps + List of apps that we know we want to skip testing for. + Principally useful for ExceptionService, SymConnectMultiplexer, etc +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [object[]]$Services = (Get-KnownWCFServices), + [Parameter(Mandatory = $false)] + [string[]]$KnownSkipWebApps = (Get-KnownSkipWebAppNames) + ) + + $logLead = Get-LogLeadName + + $failed = $false + $failedCount = 0 + + # Skip checking the ones that we know we don't need to check + foreach ($service in $Services.Where({$_.WebAppName -notin $KnownSkipWebApps})) { + $hostnameResolution = Resolve-DNSName -Name $service.Name -ErrorAction SilentlyContinue + if ($null -eq $hostnameResolution) { + # Don't stop for the first one, log 'em all + Write-Error "$logLead : Could not resolve hostname for $($service.Name) used by the WCF service $($service.WebAppName)" -ErrorAction Continue # Ensure we hit the ugly red text but don't stop even if EAP is stop + $failed = $true + $failedCount += 1 + } + } + + if ($failed) { + if (($Services.Count -gt 1) -and ($failedCount -eq $Services.Count)) { + Write-Warning "$logLead : All of the services appear to have failed, is the hosts file corrupt?" + } + # Downstream tasks expect us to throw so they will not continue + # Ex: Install_Packages -> Install-Server + throw "Could not successfully resolve hostnames on $($env:ComputerName)" + } + + Write-Host "$logLead : All services successfully tested for hostname resolution $($Services.WebAppName -join ',')" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.tests.ps1 new file mode 100644 index 0000000..4437c9a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-KnownWCFServicesResolvable.tests.ps1 @@ -0,0 +1,29 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-KnownWCFServicesResolvable" { + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { "UUT" } + + Context "hosts resolution returns null" { + It "throws when it can not resolve services" { + Mock -ModuleName $moduleForMock -CommandName Resolve-DNSName -MockWith { throw "throw" } + { Test-KnownWCFServicesResolvable } | Should Throw + } + } + + Context "hosts resolution returns anything" { + It "does not throw when all services resolve correctly" { + Mock -ModuleName $moduleForMock -CommandName Resolve-DNSName -MockWith { $true } + { Test-KnownWCFServicesResolvable } | Should Not Throw + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.ps1 new file mode 100644 index 0000000..7a50815 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.ps1 @@ -0,0 +1,32 @@ +function Test-ShouldInstallExceptionService { +<# +.SYNOPSIS + Check to see if we should install the ExceptionService based on the file version of ORB installed +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + param( + ) + + $logLead = (Get-LogLeadName) + + # ex: 2020.2.0.53 + $version = [System.Version](Get-OrbVersion) + $eolVersion = [System.Version]'2020.5' #2020.5.0.0 > 2020.5 + + if ($null -eq $version) { + throw "$logLead : ORB Version not found, can not continue. Ensure ORB was deployed to the server before this was called." + } + + # ([System.Version]'2020.5.0.1').CompareTo([system.Version]'2020.5.0.0') => 1 + # https://docs.microsoft.com/en-us/dotnet/api/system.version.compareto + # Return value Meaning + # Less than zero The current Version object is a version before value. + # Zero The current Version object is the same version as value. + # Greater than zero The current Version object is a version subsequent to value, or value is null. + if ($version.CompareTo($eolVersion) -ge 0) { + return $false + } + + return $true +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.tests.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.tests.ps1 new file mode 100644 index 0000000..7c00ee7 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-ShouldInstallExceptionService.tests.ps1 @@ -0,0 +1,32 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Test-ShouldInstallExceptionService" { + Context "No version content - Orb isn't installed or the version is wrong" { + Mock -CommandName Get-OrbVersion -ModuleName $moduleForMock -MockWith { return $null } + It "Throws on bad content (Not a version)" { + # NOTE: v5 changes the Throw match to -like instead of substring + # Updating to Pester v5 will require asterisks (*) around exception message match string + { Test-ShouldInstallExceptionService} | Should -Throw "ORB Version not found" + } + } + + Context "Returns true for an early package value (expected)" { + Mock -CommandName Get-OrbVersion -MockWith { return "2020.4.0.0" } -ModuleName $moduleForMock + It "returns true" { + ( Test-ShouldInstallExceptionService) | Should -BeTrue + } + } + + Context "Returns false for a latter package value (expected)" { + Mock -CommandName Get-OrbVersion -ModuleName $moduleForMock -MockWith { "2020.5.0.1" } + It "returns false" { + (Test-ShouldInstallExceptionService) | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Test-WebBinding.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Test-WebBinding.ps1 new file mode 100644 index 0000000..f515a4a --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Test-WebBinding.ps1 @@ -0,0 +1,43 @@ +function Test-WebBinding { +<# +.SYNOPSIS + Test if the given web binding exists on the site +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $true)] + [string]$website, + + [Parameter(Mandatory = $true)] + [string]$url, + + [Parameter(Mandatory = $false)] + [switch]$ssl + ) + + $logLead = (Get-LogLeadName) + + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + [string]$hostHeader = [string]::Empty + if ($ssl) { + $hostHeader = ("*:443:{0}" -f $url) + } + else { + $hostHeader = ("*:80:{0}" -f $url) + } + + if ($null -eq $mgr.Sites[$website]) { + Write-Host ("$logLead : Website {0} does not exist" -f $website) + return $false + } + else { + foreach ($webBinding in $mgr.Sites[$website].Bindings.bindingInformation) { + if ($webBinding -eq $hostHeader) { + return $true + } + } + return $false # binding not found + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Provider.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Provider.ps1 new file mode 100644 index 0000000..e39829e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Provider.ps1 @@ -0,0 +1,210 @@ +function Uninstall-Provider { +<# + +.SYNOPSIS + This function is used in conjunction with Alkami.Installer.Provider to uninstall legacy providers from the appropriate locations. + +.DESCRIPTION + This function is used in conjunction with Alkami.Installer.Provider to uninstall legacy providers from the appropriate locations. + This function will only attempt to remove the file that matches the provider name in the alkamimanifest.xml + It will stop the following services and restart them if they were running at the time of install: + * BankService - WCF Service + * SchedulerService - WCF Service + * CoreService - WCF Service + * SecurityManagementService - WCF Service + * NagConfigurationService - WCF Service + * Nag - Windows Service + * Radium - Windows Service + +.PARAMETER ProviderAssemblyInfo + [string] The provider's configured assembly info. + +.PARAMETER ProviderName + [string] The provider's configured name. This is not required, it will be used for display purposes only as of Alkami.PowerShell.IIS v3.1.1 + +.INPUTS + Required is the ProviderAssemblyInfo and the SourcePath. + +.OUTPUTS + This function will only Write-Information + +.EXAMPLE + Uninstall-Provider -ProviderAssemblyInfo Alkami.App.Providers.Core.Dynamic -SourcePath C:\ProgramData\chocolatey\lib\Alkami.App.Providers.Core.Dynamic + +Uninstall-Provider output may be verbose from the underlying calls being made. + +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [AllowNull()] + [string]$ProviderAssemblyInfo = $null, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string]$ProviderName = $null, + + [Parameter(Mandatory = $false)] + [AllowNull()] + [string[]]$ProviderTargets = @('BankService','CoreService', 'NotificationService','SecurityManagementService','Radium','Nag','NagConfigurationService','SchedulerService') + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + ## If it's a developer machine we can install anything + ## Don't install on MIC or WEB servers, only APP servers. + $isDeveloperMachine = (Test-IsDeveloperMachine) + $isAppServer = (Test-IsAppServer) + $isEitherAppOrDeveloper = $isDeveloperMachine -or $isAppServer + if (!$isEitherAppOrDeveloper) { + Write-Warning "$loglead : Can only install providers on app tier machines or developer machines" + return; + } + + Write-Information "$loglead : $ProviderName being uninstalled by $($env:username) on $($env:computername) at $(Get-Date)" + + ## These are the folders we will be copying into. + ## Folders missing from this list include Notification and NagConfiguration service which may need access to providers as well. + ## As ORB is moving away from a provider-centric module approach to a microservice centric approach, this should be acceptable. + $folderTargets = @() + + $orbPath = (Get-OrbPath) ## Cache the lookup + + ## If the following services are in any status other than Stopped, we want to stop, then restart them on a NON-PROD machine. + ## Production servers are always deployed to when out-of-pool, so the services are already, in theory, stopped. + $isRadiumRunning = $false + $radiumServiceName = @(Get-ServiceNamesByFragment 'Radium')[0] + if ($ProviderTargets -contains 'Radium') { + if ($radiumServiceName -ne 'Not installed') { + $isRadiumRunning = (Get-Service -Name $radiumServiceName).Status -ne 'Stopped' + $folderTargets += (Join-Path $orbPath Radium) + } + } + + $isNagRunning = $false + $nagServiceName = @(Get-ServiceNamesByFragment 'Alkami Nag')[0] + if ($ProviderTargets -contains 'Nag') { + if ($nagServiceName -ne 'Not installed') { + $isNagRunning = (Get-Service -Name $nagServiceName).Status -ne 'Stopped' + $folderTargets += (Join-Path $orbPath Nag) + } + } + + $isBankRunning = $false + if ($ProviderTargets -contains 'BankService') { + $isBankRunning = Test-IISAppPoolByName BankService + $folderTargets += (Join-Path (Join-Path $orbPath BankService) bin) + } + + $isSchedulerRunning = $false + if ($ProviderTargets -contains "SchedulerService") { + $isBankRunning = Test-IISAppPoolByName SchedulerService + $folderTargets += (Join-Path (Join-Path $orbPath SchedulerService) bin) + } + + $isCoreRunning = $false + if ($ProviderTargets -contains 'CoreService') { + $isCoreRunning = Test-IISAppPoolByName CoreService + ## CoreService is included because occasionally it may use other providers. + $folderTargets += (Join-Path (Join-Path $orbPath CoreService) bin) + } + + $isNagConfigRunning = $false + if ($ProviderTargets -contains 'NagConfigurationService') { + $isNagConfigRunning = Test-IISAppPoolByName NagConfigurationService + $folderTargets += (Join-Path (Join-Path $orbPath NagConfigurationService) bin) + } + + $isNotifyRunning = $false + if ($ProviderTargets -contains 'NotificationService') { + $isNotifyRunning = Test-IISAppPoolByName NotificationService + $folderTargets += (Join-Path (Join-Path $orbPath NotificationService) bin) + } + + $isSecMgmtRunning = $false + if ($ProviderTargets -contains 'SecurityManagementService') { + $isSecMgmtRunning = Test-IISAppPoolByName SecurityManagementService + $folderTargets += (Join-Path (Join-Path $orbPath SecurityManagementService) bin) + } + + if ($isRadiumRunning) { + Stop-AlkamiService -ServiceName $radiumServiceName + } + + if ($isNagRunning) { + Stop-AlkamiService -ServiceName $nagServiceName + } + + if ($isBankRunning) { + Stop-IISAppPoolByName BankService + Remove-DotNetTemporaryFiles Bank + } + + if ($isSchedulerRunning) { + Stop-IISAppPoolByName -Name SchedulerService + Remove-DotNetTemporaryFiles -Name SchedulerService + } + + if ($isCoreRunning) { + Stop-IISAppPoolByName CoreService + Remove-DotNetTemporaryFiles CoreService + } + + if ($isNagConfigRunning) { + Stop-IISAppPoolByName NagConfigurationService + Remove-DotNetTemporaryFiles NagConfigurationService + } + + if ($isNotifyRunning) { + Stop-IISAppPoolByName NotificationService + Remove-DotNetTemporaryFiles NotificationService + } + + if ($isSecMgmtRunning) { + Stop-IISAppPoolByName SecurityManagementService + Remove-DotNetTemporaryFiles securityManagement + } + + foreach($targetFolder in $folderTargets) { + ## Only remove the package file we intended to have in ORB in the first place. Anything else will not be loaded, or may be shared with another project already. + ## If we attempt to delete every file in the package, we may inadvertently break other application components from running. + $targetFile = (Join-Path $targetFolder "$ProviderAssemblyInfo.dll") + if (Test-Path $targetFile) { + Write-Verbose "$loglead : Removing $targetFile" + Write-Information '.'; + Remove-FileSystemItem -Path $targetFile | Out-Null + } else { + Write-Verbose "$loglead : Did not find $targetFile to remove from $targetFolder" + } + } + + if ($isBankRunning) { + Start-IISAppPoolByName BankService + } + if ($isSchedulerRunning) { + Start-IISAppPoolByName SchedulerService + } + if ($isCoreRunning) { + Start-IISAppPoolByName CoreService + } + if ($isNagConfigRunning) { + Start-IISAppPoolByName NagConfigurationService + } + if ($isNotifyRunning) { + Start-IISAppPoolByName NotificationService + } + if ($isSecMgmtRunning) { + Start-IISAppPoolByName SecurityManagementService + } + if ($isRadiumRunning) { + Start-Service -Name $radiumServiceName + } + if ($isNagRunning) { + Start-Service -Name $nagServiceName + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebApplication.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebApplication.ps1 new file mode 100644 index 0000000..132697e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebApplication.ps1 @@ -0,0 +1,137 @@ +Function Uninstall-WebApplication { +<# +.SYNOPSIS + Uninstalls a Web Application from the appropriate place + +.DESCRIPTION + Uninstalls a Web Application from the appropriate place + +.PARAMETER WebAppName + [string] The name of the web application. + +.PARAMETER IsClient + [switch] Is this package installed to client? + +.PARAMETER IsAdmin + [switch] Is this package installed to admin? + +.PARAMETER IsLegacy + [switch] Is this package installed to the legacy site? (typically Default Web Site) + +.INPUTS + WebAppName is required. + Requires one of IsClient or IsAdmin or IsLegacy + +.OUTPUTS + Various diagnostic information about the install process + +.EXAMPLE + Uninstall-WebApplication -WebAppName BankService -IsLegacy + +Various diagnostic information about the install process. + +#> + [CmdletBinding(DefaultParameterSetName='IsClient')] + Param( + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=0)] + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=0)] + [string]$WebAppName, + [Parameter(ParameterSetName='IsClient',Mandatory=$true)] + [switch]$IsClient, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true)] + [switch]$IsAdmin, + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true)] + [switch]$IsLegacy + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + ## If this is a legacy app, and we are on a web-server, we can't uninstall it + ## If this is a client or admin app, and we are on not on a web-server, we can't uninstall it. + ## If this is a developer machine we can install it + if (Test-IsDeveloperMachine) { + ## Do nothing + } elseif ((Test-IsWebServer) -and $IsLegacy) { + Write-Warning "$loglead : Can not install app-tier web applications on the web tier" + return + } elseif (!(Test-IsWebServer) -and ($IsClient -or $IsAdmin)) { + Write-Warning "$loglead : Can not install web-tier web applications on the app tier" + return + } + + Write-Host "$loglead : $WebAppName being uninstalled by $($env:username) on $($env:computername) at $(Get-Date)" + + $oldAppPoolNames = @() + $WebAppName = $WebAppName.Replace('\','/') + + ## By process of elimination, if you weren't in parameter set IsAdmin or IsLegacy, you must be in IsClient. + $parentAppName = 'WebClient' + + if ($IsAdmin) { + $parentAppName = 'WebClientAdmin' + } + + ## Test to make sure this is the valid site name. We can do this by looking for the site, if it doesn't exist, look for the path + ## C:\Orb\$parentAppName and then find any sites for that. + $findSiteIfExists = Get-IISSite -Name $parentAppName + $siteList = @($findSiteIfExists) + + if (Test-IsCollectionNullOrEmpty $siteList) { + ## Specify the path we are looking for such as c:\orb\webclient + $targetFolderPath = (Join-Path (Get-OrbPath) $parentAppName) + + $siteList = (Get-IISSitesByPath $targetFolderPath) + } + + if ($IsLegacy) { + ## Use of this function ensures that the Default Web Site exists + $defaultWebsite = (Get-DefaultWebsite) + if ($null -ne $defaultWebsite) { + $siteList += $defaultWebsite.Name + } + } + + if (Test-IsCollectionNullOrEmpty $siteList) { + Write-Host "Can not uninstall an application if no sites are found to touch. Exiting" + return + } else { + foreach ($site in $siteList) { + $sitePath = $site.Name + + try { + $existingApplication = Get-WebApplication -Name $WebAppName -Site $sitePath + if ($null -ne $existingApplication) { + $oldAppPoolNames += $existingApplication.applicationPool + Write-Host "Remove-WebApplication -Name $WebAppName -Site '$($sitePath)'" + Remove-WebApplication -Name $WebAppName -Site $sitePath + } + } catch { + Write-Verbose $_.Exception.Message + Write-Verbose "$logLead : Could not delete [$WebAppName] under [$($site)] to remove the web application" + } + } + } + + foreach($oldAppPoolName in $oldAppPoolNames) { + ## The same "wrong" app pool could have been used on other apps so only remove them if there are no referenced apps for this pool + $existingApp = (Get-Item (Join-Path "IIS:\AppPools" $oldAppPoolName)) + if ($null -ne $existingApp) { + $existingAppApplicationCount = (Get-IisAppPoolChildApplicationsCount $oldAppPoolName) + + ## The count of that outdated application pool is 0, indicating that apppool isn't in use. Delete it. + if ($existingAppApplicationCount -eq 0) { + Write-Verbose "$logLead : Removing identified and unused Web AppPool [$oldAppPoolName]" + (Remove-WebAppPool -Name $oldAppPoolName) + } else { + Write-Verbose "$logLead : Web AppPool [$oldAppPoolName] still has things attached to it. Not deleting." + } + } + } + + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebExtension.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebExtension.ps1 new file mode 100644 index 0000000..04bf7da --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-WebExtension.ps1 @@ -0,0 +1,95 @@ +Function Uninstall-WebExtension { +<# +.SYNOPSIS + Uninstall a WebExtension from the appropriate place. + +.DESCRIPTION + Uninstall a WebExtension from the appropriate place. + Will make certain assumptions based on best practices, such as locations to install to based on flags. + +.PARAMETER ExtensionName + [string] The name of the extension. Will be used to build a dynamic path. Typically the package id. + +.PARAMETER IsAdmin + [switch] Is this package installed to admin? + +.PARAMETER RemoveLogs + [switch] Remove logs if the service was running? + +.INPUTS + ExtensionName is required. + +.OUTPUTS + Various diagnostic information about the uninstall process + +.EXAMPLE + Uninstall-WebExtension -ExtensionName Alkami.Client.WebExtension.Example -SourcePath C:\ProgramData\chocolatey\lib\Alkami.Client.WebExtension.Example + +Various diagnostic information about the uninstall process. + +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$ExtensionName, + [switch]$isAdmin, + [switch]$RemoveLogs + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsDeveloperMachine) -and !(Test-IsWebServer)) { + Write-Warning "$loglead : Can not install WebExtensions on the app tier" + return + } + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + $appName = 'WebClient' + + if ($isAdmin) { + $appName = 'WebClientAdmin' + } + + $orbExtensionPath = (Join-Path (Join-Path (Join-Path (Get-OrbPath) $appName) 'Modules') $ExtensionName) + + $isRunning = (Test-IISAppPoolByName $appName) + if ($isRunning) { + Write-Host "$loglead : Stopping $appName" + Stop-WebAppPool $appName + Do + { + Start-Sleep -Milliseconds 100 + } + Until ((Get-WebAppPoolState -Name $appName).Value -eq "Stopped" ) + } + + if ($RemoveLogs) { + $logfiles = Get-LogPathsForOrbApplication $appName + + foreach($logPath in $logfiles) { + if (Test-Path $logPath) { + Remove-FileSystemItem $logPath -ErrorAction Stop + } + } + } + + ## Clear .NET Temp files associated with the site + $tempFilePath = (Get-SiteTempDirectoryPath $appName) + if ($isRunning -and !([string]::IsNullOrWhiteSpace($tempFilePath)) -and (Test-Path $tempFilePath)) { + Remove-FileSystemItem $tempFilePath -Recurse -ErrorAction Stop + } + + if (Test-Path $orbExtensionPath) { + ## Update OrbCore manifest if it exists to say that I've removed these files from the folder + Remove-FileSystemItem $orbExtensionPath -Recurse -ErrorAction Stop + } + + if ($isRunning) { + Write-Host "$loglead : Starting $appName" + Start-WebAppPool $appName + } + } +} diff --git a/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Widget.ps1 b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Widget.ps1 new file mode 100644 index 0000000..67cd030 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/Public/Uninstall-Widget.ps1 @@ -0,0 +1,96 @@ +Function Uninstall-Widget { +<# +.SYNOPSIS + Uninstalls a Widget +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$WidgetName, + [switch]$isAdmin, + [switch]$RemoveLogs + ) + process { + $loglead = (Get-LogLeadName) + + if (!(Test-IsDeveloperMachine) -and !(Test-IsWebServer)) { + Write-Warning "$loglead : Can not install widgets on the app tier" + return; + } + + if (!(Test-IsAdmin)){ + throw "You are not running as administrator. Can not continue." + } + + $appName = 'WebClient'; + + if ($isAdmin) { + $appName = 'WebClientAdmin'; + Write-Information "$loglead : Widget uninstall against Admin does not remove the bins from the bin folder. Sorry for the inconvenience." + Write-Information "$loglead : For more details, discuss with the tooling team. If you're *on the tooling team*, ask Cole or Bob." + } + + $orbAreaPath = (Join-Path (Join-Path (Join-Path (Get-OrbPath) $appName) 'Areas') $WidgetName) + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification="Variable assignment is used if isAdmin=true. False positive.")] + $orbAreaBinPath = (Join-Path $orbAreaPath "bin") + + if ($isAdmin) { + $orbAreaBinPath = (Join-Path (Join-Path (Get-OrbPath) $appName) "bin") + } + + ## Get the IIS sites by path then get the distinct application pool names for all sites bound to this path. + $orbBaseWebclientPath = Join-Path (Get-OrbPath) $appName + Write-Verbose "$loglead : Orb Base webclient path [$orbBaseWebclientPath]" + + $iisApplicationPools = @() + $iisSites = (Get-IISSitesByPath $orbBaseWebclientPath) + foreach($site in $iisSites) { + if ($site.ApplicationPool -notin $iisApplicationPools) { + $iisApplicationPools += $site.ApplicationPool + } + } + + foreach($appPool in $iisApplicationPools) { + $isRunning = (Test-IISAppPoolByName $appPool) + if ($isRunning) { + Write-Host "$loglead : Stopping $appPool" + Stop-WebAppPool $appPool + Do + { + Start-Sleep -Milliseconds 100; + } + Until ((Get-WebAppPoolState -Name $appPool).Value -eq "Stopped" ) + } + } + + if ($RemoveLogs) { + $logfiles = Get-LogPathsForOrbApplication $appName + + $logfiles | ForEach-Object { + $logPath = $_; + if (Test-Path $logPath) { + Remove-FileSystemItem $logPath -ErrorAction Stop + } + } + } + + ## Clear .NET Temp files associated with the site + $tempFilePath = (Get-SiteTempDirectoryPath $appName); + if ($isRunning -and !([string]::IsNullOrWhiteSpace($tempFilePath)) -and (Test-Path $tempFilePath)) { + Remove-FileSystemItem $tempFilePath -Recurse -ErrorAction Stop + } + + if (Test-Path $orbAreaPath) { + ## Update OrbCore manifest if it exists to say that I've removed these files from the folder + Remove-FileSystemItem $orbAreaPath -Recurse -ErrorAction Stop + } + + foreach($appPool in $iisApplicationPools) { + if ($isRunning) { + Write-Host "$loglead : Starting $appPool" + Start-WebAppPool $appPool + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.IIS/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.IIS/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.IIS/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.IIS/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.nuspec b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.nuspec new file mode 100644 index 0000000..b076c21 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.nuspec @@ -0,0 +1,26 @@ + + + + Alkami.PowerShell.PSScriptAnalyzerRules + 1.0.0 + Alkami Platform Modules - PowerShell - PSScriptAnalyzerRules + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.PSScriptAnalyzerRules + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami PSScriptAnalyzerRules module for use with PowerShell. + + PowerShell + Copyright (c) 2022 Alkami Technologies + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.psd1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.psd1 new file mode 100644 index 0000000..05988e8 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.psd1 @@ -0,0 +1,11 @@ +@{ + RootModule = 'Alkami.PowerShell.PSScriptAnalyzerRules.psm1' + ModuleVersion = '1.0.0' + GUID = '22c44e71-a473-48fc-a063-1742533ebde8' + Author = 'jcoburn' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2022 Alkami Technologies, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Invoke-CustomRuleAnalyzers','Measure-CmdletBinding','Measure-HelpSynopsis' +} diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.pssproj b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.pssproj new file mode 100644 index 0000000..567c33a --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Alkami.PowerShell.PSScriptAnalyzerRules.pssproj @@ -0,0 +1,40 @@ + + + Debug + 2.0 + {9dacfd56-8838-4be0-abf4-6db421238f53} + Exe + Alkami.PowerShell.PSScriptAnalyzerRules + Alkami.PowerShell.PSScriptAnalyzerRules + Alkami.PowerShell.PSScriptAnalyzerRules + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.PSScriptAnalyzerRules") + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Invoke-CustomRuleAnalyzers.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Invoke-CustomRuleAnalyzers.ps1 new file mode 100644 index 0000000..da051a1 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Invoke-CustomRuleAnalyzers.ps1 @@ -0,0 +1,24 @@ +function Invoke-CustomRuleAnalyzers { +<# +.SYNOPSIS + Execute analyzer rules against a specific path. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$TargetPath, + [Parameter()] + [string]$CustomRulePath, + [switch]$Recurse + ) + + if($null -eq $CustomRulePath) + { + $root = Split-Path -Path $PSScriptRoot -Parent + Write-Host "root: $root" + $CustomRulePath = Join-Path -Path $root -ChildPath "Alkami.PowerShell.PSScriptAnalyzerRules.psm1" + } + + Invoke-ScriptAnalyzer -Path $TargetPath -Recurse:$Recurse -IncludeDefaultRules -CustomRulePath $CustomRulePath + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-CmdletBinding.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-CmdletBinding.ps1 new file mode 100644 index 0000000..d7a5008 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-CmdletBinding.ps1 @@ -0,0 +1,67 @@ +function Measure-CmdletBinding { +<# +.SYNOPSIS + Add CmdletBinding to your function. +.DESCRIPTION + The CmdletBinding attribute is an attribute of functions that makes them operate like compiled cmdlets written in C#. It provides access to the features of cmdlets. + You can get more details by running: Get-Help about_Functions_CmdletBinding_Attribute +.EXAMPLE + Measure-CmdletBinding -FunctionDefinitionAst $FunctionDefinitionAst +.INPUTS + [System.Management.Automation.Language.FunctionDefinitionAst] +.OUTPUTS + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] +.NOTES + Reference: Writing Help and Comments, Windows PowerShell Best Practices. +#> + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.FunctionDefinitionAst] + $FunctionDefinitionAst + ) + + Process { + $results = @() + $message = (Get-Help $MyInvocation.MyCommand.Name).Synopsis + + try { + #region Define predicates to find ASTs. + + # Finds CmdletBinding attribute. + [ScriptBlock]$predicate = { + param ([System.Management.Automation.Language.Ast]$Ast) + + [bool]$returnValue = $false + + if ($Ast -is [System.Management.Automation.Language.AttributeAst]) { + [System.Management.Automation.Language.AttributeAst]$attrAst = $ast; + if ($attrAst.TypeName.Name -eq 'CmdletBinding') { + $returnValue = $true + } + } + + return $returnValue + } + + #endregion + + # Find if the function does not have CmdletBinding attribute + [System.Management.Automation.Language.AttributeAst[]]$attrAsts = $FunctionDefinitionAst.Find($predicate, $true) + if (!$attrAsts) { + $result = New-Object ` + -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` + -ArgumentList $message,$FunctionDefinitionAst.Extent,$PSCmdlet.MyInvocation.InvocationName,Warning,$null + + $results += $result + } + + return $results + } catch { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-HelpSynopsis.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-HelpSynopsis.ps1 new file mode 100644 index 0000000..7d8e4fe --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Measure-HelpSynopsis.ps1 @@ -0,0 +1,45 @@ +function Measure-HelpSynopsis { +<# +.SYNOPSIS + Add a SYNOPSIS keyword in your comment-based help. +.DESCRIPTION + Comment-based help is written as a series of comments. You can write comment-based help topics for end users to better understand your functions. Additionally, it's better to explain the detail about how the function works. + To fix a violation of this rule, add a .SYNOPSIS keyword in your comment-based help. You can get more details by running: Get-Help about_Comment_Based_Help +.EXAMPLE + Measure-HelpSynopsis -FunctionDefinitionAst $FunctionDefinitionAst +.INPUTS + [System.Management.Automation.Language.FunctionDefinitionAst] +.OUTPUTS + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] +.NOTES + Reference: Writing Help and Comments, Windows PowerShell Best Practices. +#> + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.FunctionDefinitionAst] + $FunctionDefinitionAst + ) + + Process { + $results = @() + $message = (Get-Help $MyInvocation.MyCommand.Name).Synopsis + + try { + if (!$FunctionDefinitionAst.GetHelpContent().Synopsis) { + $result = New-Object ` + -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` + -ArgumentList $message,$FunctionDefinitionAst.Extent,$PSCmdlet.MyInvocation.InvocationName,Warning,$null + + $results += $result + } + + return $results + } catch { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.ps1 new file mode 100644 index 0000000..21fb299 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.ps1 @@ -0,0 +1,13 @@ +function Confirm-CmdletBinding { +<# +.SYNOPSIS + Dummy function for testing analyzer rules. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$dummyParam + ) + + Write-Host "My dummy param $dummyParam." +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.tests.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.tests.ps1 new file mode 100644 index 0000000..4696530 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBinding.tests.ps1 @@ -0,0 +1,31 @@ +# Setup +$violationMessage = "Add CmdletBinding to your function." +$violationName = "Alkami.PowerShell.PSScriptAnalyzerRules\Measure-CmdletBinding" +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Setup violations +$violations = Invoke-ScriptAnalyzer $directory\Confirm-CmdletBindingWithViolations.ps1 -CustomRulePath "$directory\..\..\Alkami.PowerShell.PSScriptAnalyzerRules.psm1" | Where-Object {$_.RuleName -eq $violationName} + +# Setup No violations +$noViolations = Invoke-ScriptAnalyzer $directory\Confirm-CmdletBinding.ps1 -CustomRulePath "$directory\..\..\Alkami.PowerShell.PSScriptAnalyzerRules.psm1" | Where-Object {$_.RuleName -eq $violationName} + +# do the tests + +Describe "Measure-CmdletBinding"{ + + Context "When There Are Violations" { + It "Has a violation" { + $violations.Count | Should -Be 1 + } + + It "Has the correct description message" { + $violations[0].Message | Should -Match $violationMessage + } + } + + Context "When There Are No Violations" { + It "Doesn't have any violations" { + $noViolations.Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBindingWithViolations.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBindingWithViolations.ps1 new file mode 100644 index 0000000..b7dac17 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-CmdletBindingWithViolations.ps1 @@ -0,0 +1,13 @@ + +function Confirm-CmdletBindingWithViolations { + <# + .SYNOPSIS + Adds a Path to the System Path Environment Variable + #> + Param( + [Parameter(Mandatory = $true)] + [string]$dummyParam + ) + + Write-Host "My dummy param $dummyParam." + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.ps1 new file mode 100644 index 0000000..181a037 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.ps1 @@ -0,0 +1,12 @@ +function Confirm-HelpSynopsis { + <# + .SYNOPSIS + Dummy function for testing analyzer rules. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$dummyParam + ) + Write-Host "My dummy param $dummyParam." + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.tests.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.tests.ps1 new file mode 100644 index 0000000..49fe184 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsis.tests.ps1 @@ -0,0 +1,31 @@ +# Setup +$violationMessage = "Add a SYNOPSIS keyword in your comment-based help." +$violationName = "Alkami.PowerShell.PSScriptAnalyzerRules\Measure-HelpSynopsis" +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Setup violations +$violations = Invoke-ScriptAnalyzer $directory\Confirm-HelpSynopsisWithViolations.ps1 -CustomRulePath "$directory\..\..\Alkami.PowerShell.PSScriptAnalyzerRules.psm1" | Where-Object {$_.RuleName -eq $violationName} + +# Setup No violations +$noViolations = Invoke-ScriptAnalyzer $directory\Confirm-HelpSynopsis.ps1 -CustomRulePath "$directory\..\..\Alkami.PowerShell.PSScriptAnalyzerRules.psm1" | Where-Object {$_.RuleName -eq $violationName} + +# Do the tests + +Describe "Measure-HelpSynopsis"{ + + Context "When There Are Violations" { + It "Has a violation" { + $violations.Count | Should -Be 1 + } + + It "Has the correct description message" { + $violations[0].Message | Should -Match $violationMessage + } + } + + Context "When There Are No Violations" { + It "Doesn't have any violations" { + $noViolations.Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsisWithViolations.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsisWithViolations.ps1 new file mode 100644 index 0000000..149e41d --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/Public/Tests/Confirm-HelpSynopsisWithViolations.ps1 @@ -0,0 +1,9 @@ +function Confirm-HelpSynopsisWithViolations { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$dummyParam + ) + + Write-Host "My dummy param $dummyParam." + } \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..74befcf --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyInstall.ps1 @@ -0,0 +1,38 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + ## Ensure the module was able to load + Import-Module $id -Global; +} diff --git a/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..29b2f77 --- /dev/null +++ b/Modules/Alkami.PowerShell.PSScriptAnalyzerRules/tools/chocolateyUninstall.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.nuspec b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.nuspec new file mode 100644 index 0000000..7a5d818 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.nuspec @@ -0,0 +1,43 @@ + + + + Alkami.PowerShell.SDK + $version$ + Alkami Platform Modules - PowerShell - SDK + Alkami Technology, Inc. + Alkami Technology, Inc. + https://confluence.alkami.com/display/ISG/Alkami+SDK+Developer+Setup + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami SDK module for use with PowerShell. + + PowerShell SDK + Copyright (c) 2020 Alkami Technology, Inc. + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.psd1 b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.psd1 new file mode 100644 index 0000000..ea9b6ac --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.psd1 @@ -0,0 +1,13 @@ +@{ + RootModule = 'Alkami.PowerShell.SDK.psm1' + ModuleVersion = '1.2.4' + GUID = '4ae9228d-d627-497f-9d9d-f9fb041be57c' + Author = 'SDK' + Description = 'Alkami SDK PowerShell module will allow developers to manage their SDK environments.' + CompanyName = 'Alkami Technology, Inc.' + Copyright = '(c) 2021 Alkami Technology, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common', 'Alkami.PowerShell.Database','Alkami.PowerShell.Configuration', 'Alkami.PowerShell.Choco' + FunctionsToExport = 'Add-LocalServiceAccountsToAlkamiDatabase','Add-LocalServiceAccountsToDatabaseServer','Add-OrbHostEntries','Assert-PlatformDeveloperKitComponentsInstalled','Confirm-Software','ConvertTo-HostsFileEntry','Format-HostsFileRecord','Get-ExistingWebApps','Get-FeatureSets','Get-HostsFileAllRecords','Get-HostsFilePath','Get-InstalledSQLVersions','Get-KnownDeveloperHostsEntries','Get-LatestVersionManifest','Get-LocalPackages','Get-ManifestPath','Get-SavedInstallVersionPath','Get-SavedInstallVersions','Get-SDKSupport','Get-SDKTenant','Get-SDKUserMatrix','Install-AlkamiDeveloperPowershellTools','Install-SDKEnvironment','Install-SDKIISComponents','Install-SDKRelease','Invoke-DatabaseConfigurationAlkamiMasterTask','Invoke-DatabaseConfigurationAlkamiTenantTask','Invoke-DatabaseMigrationAlkamiMasterTask','Invoke-DatabaseMigrationAlkamiTenantTask','Invoke-SDKAlkamiMigrations','Invoke-SDKSetCompatibilityLevelAllLocalTenants','New-SDKMachineSetup','New-SDKSnapshot','Open-SDKDoc','Remove-HostsFileEntry','Remove-LegacyDatabaseUsers','Remove-OrbHostEntries','Remove-Sdk','Remove-SDKServices','Remove-SMSvcHostBlankSecurityIdentifiers','Repair-AlkamiDeveloperLoginsAndStartServices','Repair-SDKAlkamiDeveloperCertificatePermissions','Reset-SDKEnvironment','Restart-SDKServices','Restart-SDKWebClients','Save-CompleteHostsFile','Save-InstallVersions','Set-AclOnCert','Set-AlkamiConfiguration','Set-HostFileTarget','Set-SDKAppPoolUsers','Set-SDKCertificateUsers','Set-SDKDatabaseUsers','Set-SDKServicePermissions','Set-SDKServiceRecovery','Set-SDKServiceStartupType','Set-SDKTenant','Set-SDKUsers','Start-SDKServices','Stop-SDKServices','Uninstall-SDKIISComponents','Update-DeveloperModules','Update-HostsFileEntry' + AliasesToExport = 'FixLogins','New-HostsFileEntry','Update-SDKEnvironment' +} diff --git a/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.pssproj b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.pssproj new file mode 100644 index 0000000..29c4fb0 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Alkami.PowerShell.SDK.pssproj @@ -0,0 +1,60 @@ + + + Debug + 2.0 + {ed4e1a2e-ac43-4528-a996-67794bc697dd} + Exe + Alkami.PowerShell.SDK + Alkami.PowerShell.SDK + Alkami.PowerShell.SDK + Invoke-Pester; + ..\build-project.ps1 $(SolutionDir) + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/AlkamiManifest.xml b/Modules/Alkami.PowerShell.SDK/AlkamiManifest.xml new file mode 100644 index 0000000..e344507 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.SDK + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToAlkamiDatabase.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToAlkamiDatabase.ps1 new file mode 100644 index 0000000..b836ec8 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToAlkamiDatabase.ps1 @@ -0,0 +1,56 @@ +function Add-LocalServiceAccountsToAlkamiDatabase { + [CmdletBinding()] + param ( + [string]$connectionString, + [string]$databaseName + ) + + $logLead = Get-LogLeadName + + Confirm-DatabaseAccess $connectionString + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString + + $sqlConnection.Open() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "select [name] from [sys].[database_principals] where [type]='u' and [name]!='dbo';" + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + $dbNames = @() + while ($reader.Read()) { + $dbNames += $reader[0].ToString() + } + $reader.Dispose() + + $isMaster = ($databaseName -match 'AlkamiMaster') + + foreach ($account in (Get-SDKUserMatrix)) { + # This is already set in the UserMatrix to either be on the domain or use the local account info + $username = $account.DomainUsername.Trim() + # this is for the local database, not the server + $role = $account.DbRole + if (!$isMaster -or ($isMaster -and $account.IsMaster)) { + Write-Host "$logLead : Applying changes to $username on $databaseName" + $commandTexts = @() + if (!$dbNames.Contains($username)) { + $commandTexts += "CREATE USER [$username] FOR LOGIN [$username]" + } + $commandTexts += "ALTER USER [$username] WITH DEFAULT_SCHEMA=[dbo]" + $commandTexts += "ALTER ROLE [$role] ADD MEMBER [$username];" + foreach ($commandText in $commandTexts) { + try { + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + Write-Host $commandText + $command.CommandText = $commandText + $command.ExecuteNonQuery() | Out-Null + } catch { + Write-Warning $_.Exception.Message + } + } + } else { + Write-Debug "$logLead : Database does not pertain to this user [$username]" + } + } + + $sqlConnection.Close() +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToDatabaseServer.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToDatabaseServer.ps1 new file mode 100644 index 0000000..19f5ae7 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Add-LocalServiceAccountsToDatabaseServer.ps1 @@ -0,0 +1,46 @@ +function Add-LocalServiceAccountsToDatabaseServer { + param ( + [string]$connectionString + ) + + $logLead = Get-LogLeadName + + Confirm-DatabaseAccess $connectionString + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString + $sqlConnection.Open() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "select [name] from [sys].[database_principals] where [type]='u' and [name]!='dbo';" + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + $dbNames = @() + while ($reader.Read()) { + $dbNames += $reader[0].ToString() + } + $reader.Dispose() + + foreach ($account in (Get-SDKUserMatrix)) { + # This is already set in the UserMatrix to either be on the domain or use the local account info + $username = $account.DomainUsername.Trim() + # this is for the server, not the local database + $role = $account.ServerRole + Write-Host "$logLead : Applying changes to $username on database server" + $commandTexts = @() + if (!$dbNames.Contains($username)) { + $commandTexts += "CREATE LOGIN [$username] FROM WINDOWS WITH DEFAULT_DATABASE=[master];" + } + $commandTexts += "ALTER SERVER ROLE [$role] ADD MEMBER [$username];" + foreach ($commandText in $commandTexts) { + try { + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + Write-Host $commandText + $command.CommandText = $commandText + $command.ExecuteNonQuery() | Out-Null + } catch { + Write-Warning $_.Exception.Message + } + } + } + + $sqlConnection.Close() +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Add-OrbHostEntries.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Add-OrbHostEntries.ps1 new file mode 100644 index 0000000..58073ad --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Add-OrbHostEntries.ps1 @@ -0,0 +1,24 @@ +function Add-OrbHostEntries { +<# +.SYNOPSIS + Adds all the known ORB entries to the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + [OutputType([string])] + param() + + $logLead = Get-LogLeadName + + $knownHostEntries = (Get-KnownDeveloperHostsEntries) + $existingRecords = Get-Host + Write-Host "$logLead : Adding hosts file entries" + foreach ($entry in $knownHostEntries) { + "Adding hosts file with IpAddress $($entry.IpAddress) and Hostname $($entry.Hostname)" | Out-Default -Transcript | Out-Null + <# For some reason it doesn't like IpAddress and it truncates it to Ip #> + #Update-HostsFileEntry -IpAddress $entry.IpAddress -Hostname $entry.Hostname + Update-HostsFileEntry -Ip $entry.IpAddress -Hostname $entry.Hostname | Out-Default -Transcript | Out-Null + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Assert-PlatformDeveloperKitComponentsInstalled.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Assert-PlatformDeveloperKitComponentsInstalled.ps1 new file mode 100644 index 0000000..3e33f34 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Assert-PlatformDeveloperKitComponentsInstalled.ps1 @@ -0,0 +1,32 @@ +function Assert-PlatformDeveloperKitComponentsInstalled { +<# +.SYNOPSIS + Ensure the platform developer kit components are registered as installed so things can progress smoothly. +#> + [CmdletBinding()] + param() + + $logLead = Get-LogLeadName + + $installPath = "C:\programdata\chocolatey\lib\Alkami.Platform.DeveloperKit\tools\" + if (!(Test-Path -Path $installPath)) { + throw "$logLead : Alkami.Platform.DeveloperKit not installed. Please ensure it is installed." + } + + $projectLogFolder = "C:\ProgramData\Alkami\Alkami.Platform.DeveloperKit" + if (!(Test-Path $projectLogFolder)) { + New-Item -ItemType Directory -Path $projectLogFolder -Force -ErrorAction Ignore | Out-Null + } + + $wpi_msi = (Join-Path $installPath "IIS\WebPlatformInstaller_amd64_en-US.msi") + $logPath = (Join-Path $projectLogFolder "WebPiCmd.log") + + Start-Process $wpi_msi '/qn' -PassThru | Wait-Process + + & (Join-Path $installPath "IIS\ExternalDiskCache_amd64.msi") /qn /NoRestart /Log $logPath + & (Join-Path $installPath "IIS\rewrite_amd64_en-US.msi") /qn /NoRestart /Log $logPath + & (Join-Path $installPath "IIS\requestRouter_amd64.msi") /qn /NoRestart /Log $logPath + + Set-DefaultTLSVersion | Out-Null + Update-SystemPortReservations | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Confirm-Software.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Confirm-Software.ps1 new file mode 100644 index 0000000..15ecf7f --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Confirm-Software.ps1 @@ -0,0 +1,69 @@ +function Confirm-Software() { + [CmdletBinding()] + param () + + # Check for Visual Studio 2019 or 2022 + if (Test-Path -Path "C:\ProgramData\Microsoft\VisualStudio") { + $vsInstalled = $false + $vsInstances = Get-CimInstance MSFT_VSInstance + $output = "" + foreach ($vsInstance in $vsInstances) { + $splits = $vsInstance.Version.Split('.') + $version += $splits[0] + if ($version -ge 16) { + $vsInstalled = $true + } + $output += "$($vsInstance.ElementName) is installed.`n" + } + + if ($vsInstalled) { + Write-Host $output.Substring(0,$output.Length-2) + } else { + $output += "Visual Studio 2019 or higher is NOT installed. Please install before continuing SDK installation." + Write-Host $output + } + } else { + Write-Host "Visual Studio is NOT installed. Please install before continuing SDK installation."; + } + + # Check for an SQL Server Instance and verify that it is v15 (2019) or higher + $sqlVersions = Get-InstalledSQLVersions + $sqlInstalled = $false + foreach ($sqlVersion in $sqlVersions) { + $splits = $sqlVersion.Split('.') + $version += $splits[0] + if ($version -ge 15) { + $sqlInstalled = $true + } + } + + if ($sqlInstalled) { + Write-Host "Microsoft SQL Server v$sqlVersion is installed." + } else { + Write-Host "Microsoft SQL Server 2019 or greater is NOT installed. Please install before continuing SDK installation." + } + + $softwarePackages = @("7-Zip","SQL Server Management Studio","Notepad\+\+"); + foreach ($softwarePackage in $softwarePackages) { + $installed = ((gp HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match $softwarePackage).Length -gt 0 + If(-Not $installed) { + Write-Host "$($softwarePackage.Replace('\','')) is NOT installed. Please install before continuing SDK installation."; + } else { + Write-Host "$($softwarePackage.Replace('\','')) is installed." + } + } + + $chocolateyPath = "C:\ProgramData\chocolatey" + $nugetCmdLinePath = "C:\ProgramData\chocolatey\lib\NuGet.CommandLine" + if (Test-Path -Path $chocolateyPath) { + Write-Host "Chocolatey is installed" + } else { + Write-Host "Chocolatey is NOT installed. Please install before continuing SDK installation" + } + + if (Test-Path -Path $nugetCmdLinePath) { + Write-Host "NuGet Command Line is installed" + } else { + Write-Host "NuGet Command Line is NOT installed. Please install before continuing SDK installation" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/ConvertTo-HostsFileEntry.ps1 b/Modules/Alkami.PowerShell.SDK/Public/ConvertTo-HostsFileEntry.ps1 new file mode 100644 index 0000000..0d20ccc --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/ConvertTo-HostsFileEntry.ps1 @@ -0,0 +1,82 @@ +function ConvertTo-HostsFileEntry { +<# +.SYNOPSIS + Convert the given record to a defined Hosts File Entry + +.OUTPUTS + Returns a [object[]] of hosts entries. +#> + [CmdletBinding(DefaultParameterSetName = 'FromPipeline')] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'FromPipeline')] + [string]$RawRecord, + [Parameter(Mandatory = $true, ParameterSetName = 'Entries')] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true, ParameterSetName = 'Entries')] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [Parameter(Mandatory = $false, ParameterSetName = 'Entries')] + [string]$Comment + ) + begin { + $disabledKey = "#DISABLED#" + } + process { + $workingIpAddress = $IpAddress + $workingHostname = $Hostname + $workingComment = $Comment + $isDisabled = $false + + $commentSeparator = -1 + if ($PSCmdlet.ParameterSetName -eq 'FromPipeline') { + $RawRecord = $RawRecord.Trim() + + if ($RawRecord.StartsWith($disabledKey)) { + $isDisabled = $true + $RawRecord = $RawRecord.Substring($disabledKey.Length).Trim() + } + + $commentSeparator = $RawRecord.IndexOf("#") + $workingComment = "" + $keep = $false + + if ($commentSeparator -gt -1) { + $splits = $RawRecord -split '#',2 + $workingComment = $splits[1].Trim() + $RawRecord = $splits[0].Trim() + } + + if ($RawRecord.length -gt 0) { + $bits = [regex]::Split($RawRecord, "\s+") + if ($bits.count -gt 1) { + $workingIpAddress = $bits[0].Trim() + $workingHostname = $bits[1].Trim() + } + } + } + + $keep = (($workingComment -imatch 'keep') -or ($workingIpAddress -eq $null)) + $blankLine = ([string]::IsNullOrWhiteSpace($workingComment) -and [string]::IsNullOrWhiteSpace($workingIpAddress) -and ($commentSeparator -eq -1)) + + try { + $hostEntryObject = New-Object PSCustomObject -Property @{ + IpAddress = $workingIpAddress + Hostname = $workingHostname + Comment = $workingComment + Keep = $keep + BlankLine = $blankLine + IsDisabled = $isDisabled + } + + return $hostEntryObject + } catch { + Write-Warning "$logLead : Could not convert item to HostEntry object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} + +Set-Alias -Name New-HostsFileEntry -Value ConvertTo-HostsFileEntry \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Format-HostsFileRecord.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Format-HostsFileRecord.ps1 new file mode 100644 index 0000000..5c82f49 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Format-HostsFileRecord.ps1 @@ -0,0 +1,72 @@ +function Format-HostsFileRecord { +<# +.SYNOPSIS + Used to format the record object into a neatly outputted string + +.PARAMETER Records + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.OUTPUTS + One or more formatted lines to write to the hosts file +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [object[]]$Records + ) + + $logLead = Get-LogLeadName + + $lines = @() + + if (Test-IsCollectionNullOrEmpty $Records) { + return + } + + if (($null -eq $Records.IpAddress) -or ($null -eq $Records.Hostname)) { + throw "$logLead : no records found with ipaddress or hostname, can not continue" + } + + $maxHostnameWidth = 0; + foreach ($record in $Records) { + if (![string]::IsNullOrWhiteSpace($record.Hostname)) { + if ($record.Hostname.Length -gt $maxHostnameWidth) { + $maxHostnameWidth = $record.Hostname.Length; + } + } + } + + if ($maxHostnameWidth -eq 0) { + return; + } else { + $maxHostnameWidth += 5; + } + + foreach ($record in $Records) { + $line = "" + if ([string]::IsNullOrWhiteSpace($record.IpAddress)) { + if ($record.BlankLine) { + $line = ""; + } else { + $line = "# $($record.Comment)" + } + } else { + $disabledPart = "" + if ($record.IsDisabled) { + # This matches the parsing component in ConvertTo-HostsFileEntry + $disabledPart = "#DISABLED# " + } + $firstPart = $record.IpAddress.PadRight(18) + $secondPart = $record.Hostname + $thirdPart = "" + if (![string]::IsNullOrWhiteSpace($record.Comment)) { + $secondPart = $secondPart.PadRight($maxHostnameWidth) # add some space before the comment + $thirdPart = "# $($record.Comment)" # prefix the comment + } + $line = "$disabledPart$firstPart$secondPart$thirdpart" + } + $lines += $line + } + + return $lines +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-ExistingWebApps.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-ExistingWebApps.ps1 new file mode 100644 index 0000000..4904073 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-ExistingWebApps.ps1 @@ -0,0 +1,86 @@ +function Get-ExistingWebApps { + <# + Results: + PS Microsoft.PowerShell.Core\FileSystem::\\mac\home\DevRoot\SDK\alkami.powershell.sdk> Get-ExistingWebApps -IsClient + [Get-ExistingWebApps] : Found these Web Applications ["/Isotope"] + [ + { + "PhysicalPath": "C:\\ProgramData\\chocolatey\\lib\\Alkami.WebApps.Isotope\\Content\\App", + "name": "Isotope", + "applicationPool": "WebClient", + "path": "/Isotope", + "preloadEnabled": true, + "Origin": "Client" + } + ] + #> + [CmdletBinding()] + param( + [Parameter(ParameterSetName='IsClient',Mandatory=$true)] + [switch]$IsClient, + [Parameter(ParameterSetName='IsAdmin',Mandatory=$true)] + [switch]$IsAdmin, + [Parameter(ParameterSetName='IsLegacy',Mandatory=$true)] + [switch]$IsLegacy + ) + + $loglead = (Get-LogLeadName) + + $webApplications = @() + $origin = "Client" + + if ($IsLegacy) { + ## Use of this function ensures that the Default Web Site exists + $defaultWebsite = (Get-DefaultWebsite) + if ($null -eq $defaultWebsite) { + $defaultWebsite = Invoke-CommandWithRetry -ScriptBlock { return (New-DefaultWebsite) } @icwrSplat + } + ## Ensure we add this to the list of sites we are going to be installing to + $siteList += $defaultWebsite + + $parentAppName = $defaultWebsite.Name + $origin = "Legacy" + } else { + ## By process of elimination, if you weren't in parameter set IsAdmin or IsLegacy, you must be in IsClient. + $parentAppName = 'WebClient' + + if ($IsAdmin) { + $parentAppName = 'WebClientAdmin' + $origin = "Admin" + } + + $webApplications = (Get-WebApplication -Site $parentAppName) + } + + if (Test-IsCollectionNullOrEmpty $webApplications) { + Write-Host "$logLead : Can't find any Web Applications bound to: [$parentAppName]" + #throw "$logLead : Can't find any Web Applications bound to: [$parentAppName]" + } else { + Write-Host "$logLead : Found these Web Applications [`"$(($webApplications.path) -join '`",`"')`"] in $($parentAppName)" + } + + $webApps = @() + + foreach ($webApplication in $webApplications) { + <#Gather the following information and put into an object for later consumption + "path": "/Isotope", + "applicationPool": "WebClient", + "preloadEnabled": true, + "PhysicalPath": "C:\\ProgramData\\chocolatey\\lib\\Alkami.WebApps.Isotope\\content\\App"#> + + $webApp = @{ + name = $webApplication.path -Replace '[/]', '' # remove leading '\' + path = $webApplication.path + applicationPool = $webApplication.applicationPool + preloadEnabled = $webApplication.preloadEnabled + PhysicalPath = $webApplication.PhysicalPath + Origin = $origin + NeedsShared = $false + } + + $webApps += $webApp + } + + #Write-Host (ConvertTo-Json $webApps) + return $webApps +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-FeatureSets.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-FeatureSets.ps1 new file mode 100644 index 0000000..49f9b7f --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-FeatureSets.ps1 @@ -0,0 +1,88 @@ +function Get-FeatureSets() { + $defaultFeatures = "features,api" + + if ($Latest) { + $Features = (Get-SavedInstallVersions)[1] + $Version = Get-LatestVersionManifest + if ([string]::IsNullOrWhiteSpace($Features) -or $Features.Count -lt 2) { + $Features = ($defaultFeatures -split ',') + } + } + + Write-Host "$logLead : You selected the version [$Version]" + if ($null -ne $Features) { + foreach ($feature in $Features) { + Write-Host "$logLead : You selected to install all packages in [$feature]" + } + } + + if ($Features -eq 0) { + # Override a bug from syntactic doing-things + $Features = ($defaultFeatures -split ',') + } + + #$manifestFolder = (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath "Manifests") + #"Manifest folder: $manifestFolder" | Out-Default -Transcript | Out-Null + #if (!(Test-Path -Path $manifestFolder)) { + # $manifestFolder = (Join-Path -Path $PSScriptRoot -ChildPath "Manifests") + #} + + $manifestFolder = Get-ManifestPath + + if (!(Test-Path -Path $manifestFolder)) { + throw "$logLead : Can't find the manifest folder" + } + + $files = @() + $files += Get-Item -Path (Join-Path -Path $manifestFolder -ChildPath "$version.json") + if ($null -ne $Features) { + foreach ($feature in $Features) { + $path = (Join-Path -Path $manifestFolder -ChildPath "$version.$feature.json") + if (Test-Path -Path $path) { + $files += Get-Item -Path $path + } else { + Write-Warning "Could not find a file for [$version.$feature] - Did it get updated to the latest version?" + } + } + } + + Save-InstallVersions -Version $Version -Features $Features + + if ($env:ComputerName -eq "ALK-VM3678") { + throw "don't break cole's computer" + } + + Write-Verbose "$logLead : Will read in the files in [$($files.FullName)]" + + $packageLists = New-Object -TypeName "System.Collections.ArrayList" + $removePackages = @() + foreach ($file in $files) { + $file | Out-Default -Transcript | Out-Null + $packageLists.Add((ConvertFrom-Json -InputObject (Get-Content -Path $file -Raw))) | Out-Null + } + + foreach ($packageList in $packageLists) { + foreach ($removePackage in $packageList.RemovePackages) { + if ($null -ne $removePackage) { + $removePackages += $removePackage + } + } + } + + $removePackages = $removePackages | Sort-Object | Get-Unique + + $allPackages = New-Object -TypeName "System.Collections.ArrayList" + + foreach ($tier in $tiers) { + $collectedTierPackages = @() + foreach ($packageList in $packageLists) { + $collectedTierPackages += $packageList.Tiers[$tier] + } + foreach ($package in $collectedTierPackages) { + Write-Verbose "$logLead : $tier -> $($package.Id) $($package.Version)" + } + $allPackages.Add($collectedTierPackages) | Out-Null + } + + return $allPackages, $removePackages +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFileAllRecords.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFileAllRecords.ps1 new file mode 100644 index 0000000..b89e06c --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFileAllRecords.ps1 @@ -0,0 +1,18 @@ +function Get-HostsFileAllRecords { +<# +.SYNOPSIS + Returns all hosts file entries as a list of objects of the format: + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.OUTPUTS + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.LINK + ConvertTo-HostsFileEntry +#> + [CmdletBinding()] + [OutputType([object[]])] + param() + + return (Get-Content -Path (Get-HostsFilePath)) | ConvertTo-HostsFileEntry +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFilePath.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFilePath.ps1 new file mode 100644 index 0000000..92df29f --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-HostsFilePath.ps1 @@ -0,0 +1,15 @@ +function Get-HostsFilePath { +<# +.SYNOPSIS + Return the known location of the hosts file. This function is platform aware. +#> + [CmdletBinding()] + [OutputType([string])] + param() + + # if (Test-IsWindowsPlatform) { + return "$env:windir\System32\drivers\etc\hosts" +# } else { +# return "/etc/hosts" +# } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-InstalledSQLVersions.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-InstalledSQLVersions.ps1 new file mode 100644 index 0000000..8aa6a7e --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-InstalledSQLVersions.ps1 @@ -0,0 +1,14 @@ +Function Get-InstalledSQLVersions() { + [CmdletBinding()] + param () + + $versions = @() + $instances = (get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server').InstalledInstances + foreach ($instance in $instances) + { + $instanceName = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$instance + $versions += (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceName\Setup").Version + } + + return $versions | Sort-Object | Get-Unique +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-KnownDeveloperHostsEntries.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-KnownDeveloperHostsEntries.ps1 new file mode 100644 index 0000000..b23963b --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-KnownDeveloperHostsEntries.ps1 @@ -0,0 +1,29 @@ +function Get-KnownDeveloperHostsEntries { +<# +.SYNOPSIS + Returns a list of known developer host entries for ORB installation default webapps +#> + [CmdletBinding()] + [OutputType([string])] + param() + + return @( + @{ IpAddress = "127.0.0.1"; Hostname = "AuditService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "BankService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ContentService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "CoreService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ExceptionService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "IP-STS"; } + @{ IpAddress = "127.0.0.1"; Hostname = "MessageCenterService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "NagConfigurationService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "NotificationService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "RP-STS"; } + @{ IpAddress = "127.0.0.1"; Hostname = "Scheduler"; } + @{ IpAddress = "127.0.0.1"; Hostname = "SecurityManagementService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "STSConfiguration"; } + @{ IpAddress = "127.0.0.1"; Hostname = "redis-18620.redis.corp.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ip.dev.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "developer.dev.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "admin-developer.dev.alkamitech.com"; } + ) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-LatestVersionManifest.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-LatestVersionManifest.ps1 new file mode 100644 index 0000000..13d69b6 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-LatestVersionManifest.ps1 @@ -0,0 +1,27 @@ +function Get-LatestVersionManifest { + #$manifestFolder = (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath "Manifests") + #if (!(Test-Path -Path $manifestFolder)) { + # $manifestFolder = (Join-Path -Path $PSScriptRoot -ChildPath "Manifests") + #} + + $manifestFolder = Get-ManifestPath + + if (!(Test-Path -Path $manifestFolder)) { + throw "$logLead : Can't find the manifest folder" + } + + $files = Get-ChildItem -Path (Join-Path $manifestFolder "*.json") + $fileNames = @() + foreach ($file in $files) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($file.FullName) + $fileNameSplits = $fileName -split '\.' + $versionMajor = $fileNameSplits[0] + $versionMinor = $fileNameSplits[1] + $version = "$versionMajor.$versionMinor" + if ($fileNames -notcontains $version) { + Write-Verbose "$logLead : Found version $version" + $fileNames += $version + } + } + return $fileNames | Sort-Object | Select-Object -Last 1 +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-LocalPackages.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-LocalPackages.ps1 new file mode 100644 index 0000000..a0d3a2c --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-LocalPackages.ps1 @@ -0,0 +1,73 @@ +function Get-LocalPackages() { +<# +.SYNOPSIS +Set the recovery options to restart for all Alkami SDK services. +#> + [CmdletBinding()] + param( + [System.Collections.ArrayList] [Parameter(Mandatory=$true)] $Packages + ) + + Write-Host "$logLead : Gathering locally installed package details" + # highly brittle command here with the choco list -lr + # -l = local packages only + # -r = limit output (things are pipe delimited, id|version) + $allLocallyInstalledPackagesRaw = (choco list -lr) + $allLocallyInstalledPackages = @{} + $localInstalledPackages = @() + foreach ($package in $allLocallyInstalledPackagesRaw) { + if ($package.IndexOf('|') -eq -1) { + continue + } + $splits = $package -split '\|' + $id = $splits[0] + $version = $splits[1] + $allLocallyInstalledPackages[$id] = $version + $localInstalledPackages += $splits[0] + Write-Verbose "$id $version" + } + Write-Host "$logLead : Locally installed package details gathered" + + # When we built the tier package lists via automation, we ensured that each package is only in one tier/feature location, so it can't be in two lists at once. + # Because we rely on automation, we are not checking that again + $newPackagesToInstall = @() + $packagesToUpgrade = @() + $packagesToRemove = @() + $tierCount = 0 + foreach ($tier in $Packages) { + foreach ($package in $tier) { + if ($null -ne $allLocallyInstalledPackages[$package.Id]) { + # package is installed locally + if ($allLocallyInstalledPackages[$package.Id] -eq $package.Version) { + # no need to touch the package, it's already here + $packageVersion = "[@($package.Version)]" + if ($package.VersionCanTakeAny) { + $packageVersion = "the latest available version" + } + "Found already existing matched version of [$($package.Id)] @ [$($allLocallyInstalledPackages[$package.Id])]" | Out-Default -Transcript | Out-Null + #$transcriptOfChanges += "Upgrading package [$($package.Id)] from version [$($allLocallyInstalledPackages[$package.Id])] to $packageVersion" + } else { + $package | Add-Member -NotePropertyName 'Tier' -NotePropertyValue $tierCount + $packagesToUpgrade += $package + Write-Verbose "$logLead : Upgrading $($package.Tier) -> $($package.Id) $($package.Version)" + "Upgrading $($package.Tier) -> $($package.Id) $($package.Version)" | Out-Default -Transcript | Out-Null + } + } else { + # package is not installed locally + $package | Add-Member -NotePropertyName 'Tier' -NotePropertyValue $tierCount + $newPackagesToInstall += $package + Write-Verbose "$logLead : Installing $($package.Tier) -> $($package.Id) $($package.Version)" + } + } + $tierCount+=1 + } + + foreach ($package in $removePackages) { + Write-Host "Checking to see if $($package) is installed" + if ($localInstalledPackages -Contains $package) { + Write-Verbose "Yep it is showing it is installed" + $packagesToRemove += $package + } + } + return $packagesToUpgrade, $newPackagesToInstall, $packagesToRemove +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-ManifestPath.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-ManifestPath.ps1 new file mode 100644 index 0000000..11e7982 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-ManifestPath.ps1 @@ -0,0 +1,6 @@ +function Get-ManifestPath { + [CmdletBinding()] + param() + + return "C:\AlkamiSDK\Manifests" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-SDKSupport.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKSupport.ps1 new file mode 100644 index 0000000..b5b96eb --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKSupport.ps1 @@ -0,0 +1,104 @@ +function Get-SDKSupport() { +<# +.SYNOPSIS + Gets system information and all SDK/Alkami installed packages along with the Windows Experience Index +#> + [CmdletBinding()] + param () + + $transcriptFilename = "Alkami.SDK.Support.$(Get-Date -Format "yyyyMMddhhmm").txt" + $transcriptPath = Join-Path -Path (Get-OrbLogsPath) -ChildPath $transcriptFilename + Start-Transcript -Path $transcriptPath + + "`nComputer Info:" | Out-Default -Transcript | Out-Null + $diskInfo = Get-PhysicalDisk + $sysInfo = systemInfo + $processorInfo = Get-CimInstance -ComputerName Localhost -Class CIM_Processor -ErrorAction Stop | Select-Object + $hotFixes = Get-HotFix + Write-Host "Generating Experience Index. This will take a while but it is still working, promise." + $perfIndex = winsat formal + $perfIndexReport = get-wmiobject -class win32_winsat + + "`tHardware Information:" | Out-Default -Transcript | Out-Null + "`t`tDisk:" | Out-Default -Transcript | Out-Null + "`t`t`tType: $($diskInfo.MediaType)" | Out-Default -Transcript | Out-Null + "`t`t`tName: $($diskInfo.FriendlyName)" | Out-Default -Transcript | Out-Null + "`t`t`tManufacturer: $(($diskInfo.CimInstanceProperties | Where-Object -Property Name -eq 'Manufacturer' | Select-Object -Property Value).Value)" | Out-Default -Transcript | Out-Null + "`t`t`tModel: $($diskInfo.Model)" | Out-Default -Transcript | Out-Null + "`t`t`tStatus: $($diskInfo.HealthStatus)" | Out-Default -Transcript | Out-Null + + "`n`t`tProcessor:" | Out-Default -Transcript | Out-Null + "`t`t`tManufacturer: $($processorInfo.Manufacturer)" | Out-Default -Transcript | Out-Null + "`t`t`tName: $($processorInfo.Name)" | Out-Default -Transcript | Out-Null + "`t`t`tNumber Of Cores: $($processorInfo.NumberOfCores)" | Out-Default -Transcript | Out-Null + "`t`t`tNumber Of LogicalProcessors: $($processorInfo.NumberOfLogicalProcessors)" | Out-Default -Transcript | Out-Null + "`t`t`tCurrent Clock Speed: $($processorInfo.CurrentClockSpeed)" | Out-Default -Transcript | Out-Null + "`t`t`tMax Clock Speed: $($processorInfo.MaxClockSpeed)" | Out-Default -Transcript | Out-Null + "`t`t`tThread Count: $($processorInfo.ThreadCount)" | Out-Default -Transcript | Out-Null + + "`n`t`tMemory:" | Out-Default -Transcript | Out-Null + "`t`t`tTotal Physical Memory: $((($sysInfo | Select-String 'Total Physical Memory:').ToString().Split(':'))[1].Trim())" | Out-Default -Transcript | Out-Null + "`t`t`tAvailable Physical Memory: $((($sysInfo | Select-String 'Available Physical Memory:').ToString().Split(':'))[1].Trim())" | Out-Default -Transcript | Out-Null + + "`n`t`tOperating System Information:" | Out-Default -Transcript | Out-Null + "`t`t`tName: $((($sysInfo | Select-String 'OS Name:').ToString().Split(':'))[1].Trim())" | Out-Default -Transcript | Out-Null + "`t`t`tVersion: $(((ConvertTo-Json ($sysInfo | Select-String 'OS Version:')[0]) | ConvertFrom-Json).Line.Split(':')[1].Trim())" | Out-Default -Transcript | Out-Null + "`t`t`tHotFix's Installed:" | Out-Default -Transcript | Out-Null + foreach ($hotfix in $hotFixes) { + "`t`t`t$($hotfix.HotFixID)" | Out-Default -Transcript | Out-Null + } + + "`nWindows Experience Index:" | Out-Default -Transcript | Out-Null + "`tOverall Experience: $($perfIndexReport.WinSPRLevel)" | Out-Default -Transcript | Out-Null + "`tCPU: $($perfIndexReport.CPUScore)" | Out-Default -Transcript | Out-Null + "`tDisk: $($perfIndexReport.DiskScore)" | Out-Default -Transcript | Out-Null + "`tMemory: $($perfIndexReport.MemoryScore)" | Out-Default -Transcript | Out-Null + "`tGraphics: $($perfIndexReport.GraphicsScore)" | Out-Default -Transcript | Out-Null + "`tDirect 3D: $($perfIndexReport.D3DScore)" | Out-Default -Transcript | Out-Null + + "`nInstalled Packages:" | Out-Default -Transcript | Out-Null + # Get installed packages + $allLocallyInstalledPackages = (choco list -lr) + foreach ($package in $allLocallyInstalledPackages) { + if ($package.IndexOf('|') -eq -1) { + continue + } + $splits = $package -split '\|' + $id = $splits[0] + $version = $splits[1] + "`t$id $version" | Out-Default -Transcript | Out-Null + } + + # Get Installed Alkami services and their current state + $alkamiServices = Get-Service | Where-Object {($_.Name -match "Alkami.+") -or ($_.Name -match "redis-+")} + "`nAlkami Services:" | Out-Default -Transcript | Out-Null + "`tStatus:`t`tName:" | Out-Default -Transcript | Out-Null + foreach ($service in $alkamiServices) { + "`t$($service.Status)`t`t$($service.Name)" | Out-Default -Transcript | Out-Null + } + + # Get Installed software + # Get all installed Visual Studio instances + "`nVisual Studio Instances:" | Out-Default -Transcript | Out-Null + + $vsInstances = Get-CimInstance MSFT_VSInstance + foreach ($vsInstance in $vsInstances) { + "`t$($vsInstance.ElementName) Version:$($vsInstance.Version) is installed." | Out-Default -Transcript | Out-Null + } + + # Get all installed MS SQL instances + "`nMicrosoft SQL Server Instances:" | Out-Default -Transcript | Out-Null + + $sqlInstances = (get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server').InstalledInstances + foreach ($sqlInstance in $sqlInstances) + { + $instanceName = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$sqlInstance + "`tInstance Name: $($sqlInstance)" | Out-Default -Transcript | Out-Null + "`tEdition: $((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceName\Setup").Edition)" | Out-Default -Transcript | Out-Null + "`tVersion: $((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceName\Setup").Version)" | Out-Default -Transcript | Out-Null + "`tPatch Level: $((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceName\Setup").PatchLevel)`n" | Out-Default -Transcript | Out-Null + } + + "`n" + Stop-Transcript +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-SDKTenant.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKTenant.ps1 new file mode 100644 index 0000000..d64200e --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKTenant.ps1 @@ -0,0 +1,32 @@ +function Get-SDKTenant { +<# +.SYNOPSIS +Get a list of configured tenants on a target server environment + +.DESCRIPTION +Get a list of tenants that can be configured locally. Will also show locally configured tenants + +.PARAMETER SourceName +The server target. + +.PARAMETER DatabaseName +The target "AlkamiMaster" database to pull available tenants from. It will default to AlkamiMaster_Dev1 + +.OUTPUTS +Object of tenants + +.EXAMPLE +Get-SDKTenant -SourceName "remote_db" -MasterDatabaseName "AlkamiMaster_Dev1" + +.NOTES +General notes +#> + [CmdletBinding()] + param ( + [string] $SourceName = "localhost", + [string] $MasterDatabaseName = "AlkamiMaster" + ) + + $masterConnectionString = "data source=$SourceName;Integrated Security=SSPI; Database=$MasterDatabaseName;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;"; + Get-FullTenantListFromServer -connectionString $masterConnectionString; +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-SDKUserMatrix.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKUserMatrix.ps1 new file mode 100644 index 0000000..986771b --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-SDKUserMatrix.ps1 @@ -0,0 +1,78 @@ +function Get-SDKUserMatrix { +<# +.SYNOPSIS + Get the matrix of SDK Users, AppPoolIdentity, DomainUsername if on the domain, tenant DbRole, global database ServerRole, if it's a gMSA Account, and if it affects the AlkamiMaster table + +.DESCRIPTION + gMSA account = Group Managed Service Account, a Microsoft AD component + DbRole = Tenant database role as assigned (and for IsMaster, those on AlkamiMaster as well) + ServerRole = SQL Server/instance role assigned if created + +.PARAMETER Force + Alias -Refresh + Used to refetch the list of accounts to validate domain membership +#> + param( + [Parameter()] + [Alias('Refresh')] + [switch]$Force + ) + + if ($Force) { + # Clear it so we reprocess + $global:sqlUserAccountList = $null + } + + if ($null -ne $global:sqlUserAccountList) { + # We calculate the AD group membership here, so don't spend the time or network resources re-querying for that + return $global:sqlUserAccountList + } + + $accountList = @( + @{ Username="IIS APPPOOL\AuditService"; DomainUsername="CORP\dev.audit$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\BankService"; DomainUsername="CORP\dev.bank$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$true; }, + @{ Username="IIS APPPOOL\ContentService"; DomainUsername="CORP\dev.content$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\CoreService"; DomainUsername="CORP\dev.core$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\MessageCenterService"; DomainUsername="CORP\dev.notify$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\NagConfigurationService"; DomainUsername="CORP\dev.nag$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $true; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\NotificationService"; DomainUsername="CORP\dev.notify$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\RP-STS"; DomainUsername="CORP\dev.dbms$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $true; IsGmsaAccount = $false; IsMaster=$true; }, + @{ Username="IIS APPPOOL\STSConfiguration"; DomainUsername="CORP\dev.stsconfig$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$true; }, + @{ Username="IIS APPPOOL\SchedulerService"; DomainUsername="CORP\dev.radium$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $true; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="IIS APPPOOL\SecurityManagementService"; DomainUsername="CORP\dev.securitymgr$"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$false; }, + @{ Username="NT AUTHORITY\LOCAL SERVICE"; DomainUsername="NT AUTHORITY\LOCAL SERVICE"; DbRole="db_owner"; ServerRole="sysadmin"; RequiresCertAccess = $false; IsGmsaAccount = $false; IsMaster=$true; } + ) + + # because shenanigans involving modified collections + $returnList = @() + + foreach ($account in $accountList) { + if ($account.Username -match 'LOCAL SERVICE') { + # Don't try to see if Local Service is on the domain, it's _local_ for a reason + } else { + $serviceAccount = $null + try { + $serviceAccount = (Get-ADServiceAccount -Identity ($account.DomainUsername -split '\\')[1] -ErrorAction SilentlyContinue) + } catch { <# NOP #> } + if ($null -ne $serviceAccount) { + # found an AD account that matches, use that as a group managed service account (it's a service account, per above) + $account.IsGmsaAccount = $true + } else { + $account.DomainUsername = $account.Username + } + } + + if ($account.Username -match "IIS APPPOOL") { + $account.AppPoolName = ($account.Username -split '\\')[1] + } else { + $account.AppPoolName = '' + } + + $returnList += $account + } + + # save time recalculating + $global:sqlUserAccountList = $returnList + + return $returnList +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersionPath.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersionPath.ps1 new file mode 100644 index 0000000..3062036 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersionPath.ps1 @@ -0,0 +1,6 @@ +function Get-SavedInstallVersionPath { + [CmdletBinding()] + param() + + return "C:\ProgramData\Alkami\SDK\saved_version.txt" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersions.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersions.ps1 new file mode 100644 index 0000000..7470ba7 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Get-SavedInstallVersions.ps1 @@ -0,0 +1,11 @@ +function Get-SavedInstallVersions { + [CmdletBinding()] + param () + + # TODO: Read this from the file used in Save-InstallVersions + $filePath = Get-SavedInstallVersionPath + if (!(Test-Path -Path $filePath)) { + return $null,$null + } + return Get-Content -Path $filePath +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Install-AlkamiDeveloperPowershellTools.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Install-AlkamiDeveloperPowershellTools.ps1 new file mode 100644 index 0000000..03f4524 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Install-AlkamiDeveloperPowershellTools.ps1 @@ -0,0 +1,52 @@ +function Install-AlkamiDeveloperPowershellTools { +<# +.SYNOPSIS + This function is a thin shim of how to update the modules. +#> +# TODO: REMOVE ME + [CmdletBinding()] + [OutputType([void])] + param( + [switch]$IncludeInstallers, + [switch]$All + ) + + + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + Write-Warning "!! THIS FEATURE WILL BE DEPRECATED. PLEASE USE Update-DeveloperModules !!" + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + if ($All) { + $IncludeInstallers = $true + } + + $knownModuleList = @( + 'Alkami.PowerShell.Common' + 'Alkami.PowerShell.AD' + 'Alkami.PowerShell.Configuration' + 'Alkami.PowerShell.Services' + 'Alkami.PowerShell.Database' + 'Alkami.PowerShell.ServerManagement' + 'Alkami.PowerShell.Choco' + 'Alkami.PowerShell.ServiceFabric' + 'Alkami.PowerShell.IIS' + ) + if ($IncludeInstallers) { + $knownModuleList += @( + 'Alkami.Installer.Widget' + 'Alkami.Installer.Provider' + 'Alkami.Installer.WebExtension' + 'Alkami.Installer.WebApplication' + 'Alkami.Installer.Services' + 'Alkami.Installer.WebSite' + 'Alkami.Installer.LegacyUtility' + 'Alkami.Installer.Hotfix' + 'Alkami.Installer.Migration' + 'Alkami.SRE.MigrationUtility' + ) + } + + choco upgrade ($knownModuleList -join ';') -fy + + Get-Module Alkami* | Remove-Module -Force +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Install-SDKEnvironment.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKEnvironment.ps1 new file mode 100644 index 0000000..2b6c816 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKEnvironment.ps1 @@ -0,0 +1,36 @@ +function Install-SDKEnvironment { +<# +.SYNOPSIS +Install a working environment on this local machine + +.DESCRIPTION +The SDK has a basic install without features that results in a lean environment for custom development. Use the -WithFeatures flag when you want to install the full SDK with Sales Demo features. + +.EXAMPLE +Install-SDKEnvironment -WithFeatures $true + +.NOTES +General notes +#> + [CmdletBinding()] + param( + [bool] $WithFeatures + ) + process { + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + Write-Warning "!! THIS FEATURE WILL BE DEPRECATED. PLEASE USE Install-SDKRelease !!" + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + Set-AlkamiConfiguration -Configuration StartServicesOnInstall -Off + + # TODO: When we move to using Experimental, change the chocoInstall of Alkami.MachineSetup.SDK to "please install with the new command thxbai" + + if($WithFeatures){ + & choco upgrade alkami.machinesetup.sdk.features -y; + } else { + & choco upgrade alkami.machinesetup.sdk -y; + } + } +} + +Set-Alias -Name Update-SDKEnvironment -Value Install-SDKEnvironment diff --git a/Modules/Alkami.PowerShell.SDK/Public/Install-SDKIISComponents.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKIISComponents.ps1 new file mode 100644 index 0000000..afd795e --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKIISComponents.ps1 @@ -0,0 +1,63 @@ +function Install-SDKIISComponents { + [CmdletBinding()] + param() + + $logLead = Get-LogLeadName + + Import-Module WebAdministration + + ## Remove existing sites along the specified path names + # This is being redone to match what is in prod, instead of QA + # Developers are only expected to have a handful of sites, so making this change is beneficial for developers so they continue to match production conifguration + $siteNamesToKeep = @('WebClient','WebClientAdmin','IPSTS','DefaultWebSite','Default Web Site', 'Eagle Eye', 'CoreDashboard', 'Orion') + $sitesToTest = @() + $sitesToTest += Get-IISSitesByPath -Path "C:\Orb\WebClient" + $sitesToTest += Get-IISSitesByPath -Path "C:\Orb\WebClientAdmin" + $sitesToTest += Get-IISSitesByPath -Path "C:\Orb\IPSTS" + foreach ($site in $sitesToTest) { + if ($site.Name -in $siteNamesToKeep) { + continue + } + + #Remove-IISSite -Name $site.Name + Remove-Item IIS:\Sites\$site.Name + } + + $forciblyCreate = @('WebClient', 'WebClientAdmin', 'IPSTS') + foreach ($create in $forciblyCreate) { + $appPool = (Get-AlkamiWebAppPool $create) + if ($null -eq $appPool) { + $appPool = (New-AlkamiWebAppPool $create) + } + } + + ## Loop tenants for site names to create + $tenants = Get-FullTenantListFromServer + + # Handle the use-case where one tenant has multiple signatures in the smae field + # This will auto-array via PS so we just end up with a list of tenant signatures + # Yay magic + $clientSignatures = $tenants.Signature -split ',' + $adminSignatures = $tenants.AdminSignature -split ',' + + foreach ($clientSignature in $clientSignatures) { + New-ClientWebBinding -ClientUrl $clientSignature -doCombineClientAppPools $true + } + foreach ($adminSignature in $adminSignatures) { + New-AdminWebBinding -AdminUrl $adminSignature -doCombineAdminAppPools $true + } + + New-IPSTSWebBinding -ipstsUrl 'ip.dev.alkamitech.com' -DoCombineIPSTSAppPools $true + + New-AppTierWebApplications + + # Override the global to only install Radium, not Nag. +$global:appTierServices = @( + @{ FolderName = 'Radium'; AssemblyInfo = 'Alkami.App.Radium.WindowsService'; Name = "Alkami Radium Scheduler Service"; User = "REPLACEME"; Password = "REPLACEME"; IsGMSAAccount = $true; Binary = $basePath + "\Radium\Alkami.App.Radium.WindowsService.exe"; } +) + $radiumServiceName = $global:appTierServices.Name + if (Stop-AlkamiService -ServiceName $radiumServiceName) { + Write-Warning "$logLead : Radium may be stopped, you may need to start it manually if this was not called as part of a deploy/install" + } + New-AppTierWindowsServices +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Install-SDKRelease.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKRelease.ps1 new file mode 100644 index 0000000..adb1e52 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Install-SDKRelease.ps1 @@ -0,0 +1,347 @@ +function Install-SDKRelease { +<# +.SYNOPSIS + Install a release of SDK with some set of feature packages, where they exist +#> + [CmdletBinding(DefaultParameterSetName = 'VersionAndFeatures')] + param ( + [Parameter(ParameterSetName = 'VersionAndFeatures')] + [ArgumentCompleter({ + $manifestFolder = (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath "Manifests") + if (!(Test-Path -Path $manifestFolder)) { + $manifestFolder = (Join-Path -Path $PSScriptRoot -ChildPath "Manifests") + } + $files = Get-ChildItem -Path (Join-Path $manifestFolder "*.json") + $fileNames = $files | Foreach-Object { return ([System.IO.Path]::GetFileNameWithoutExtension($_) -split '\.')[0..1] -join '.' } | Sort-Object | Get-Unique + return $filenames | Foreach-Object { $_ } + })][string]$Version = ((Get-SavedInstallVersions) -split "`n")[0], + + [Parameter(ParameterSetName = 'VersionAndFeatures')] + [ArgumentCompleter({ + $manifestFolder = (Join-Path -Path (Split-Path $PSScriptRoot -Parent) -ChildPath "Manifests") + if (!(Test-Path -Path $manifestFolder)) { + $manifestFolder = (Join-Path -Path $PSScriptRoot -ChildPath "Manifests") + } + $files = Get-ChildItem -Path (Join-Path $manifestFolder "*.json") + $fileNames = $files | Foreach-Object { return ([System.IO.Path]::GetFileNameWithoutExtension($_) -split '\.')[2] } | Sort-Object | Get-Unique + return $filenames | Foreach-Object { $_ } + })][string[]]$Features = ((Get-SavedInstallVersions) -split "`n")[1], + + [Parameter(ParameterSetName = 'Latest')] + [switch]$Latest + ) + + $logLead = Get-LogLeadName + $tiers = 0..4 + + if ($null -ne (Get-Command -Name Set-EnvironmentVariable -ErrorAction Ignore)) { + Set-AlkamiConfiguration -Configuration StartServicesOnInstall -Off + } + + $transcriptFilename = "Alkami.SDK.InstallRelease.$(Get-Date -Format "yyyyMMddhhmm").txt" + $transcriptPath = Join-Path -Path (Get-OrbLogsPath) -ChildPath $transcriptFilename + Start-Transcript -Path $transcriptPath + + # Ensure hosts file entries exist for later + Add-OrbHostEntries + + # Install/Upgrade Alkami.SDKRelease.Manifests package + $chocoSplat = @( + "upgrade" + "Alkami.SDKRelease.Manifests" + "-i" + "-y" + "--no-progress" + "--limit-output" + ) + Invoke-CallOperatorWithPathAndParameters -Path Choco -Arguments $chocoSplat *>&1 | Out-Default -Transcript | Out-Null + + try { + $allPackages, $removePackages = Get-FeatureSets + Write-Verbose (ConvertTo-Json $allPackages) + $packagesToUpgrade, $newPackagesToInstall, $packagesToRemove = Get-LocalPackages -Packages $allPackages + + Write-Host "$logLead : Found [$($packagesToUpgrade.Count)] existing packages to upgrade" + foreach ($packageToUpgrade in $packagesToUpgrade) { + "Upgrade $($packageToUpgrade.Tier) -> $($packageToUpgrade.id) $($packageToUpgrade.Version)" | Out-Default -Transcript | Out-Null + } + + Write-Host "$logLead : Found [$($newPackagesToInstall.Count)] new packages to install" + foreach ($packageToInstall in $newPackagesToInstall) { + "Install $($packageToInstall.Tier) -> $($packageToInstall.id) $($packageToInstall)." | Out-Default -Transcript | Out-Null + } + + if ($packagesToRemove.Count -gt 0) { + Write-Host "$logLead : Found [$($packagesToRemove.Count)] existing packages to remove" + foreach ($package in $packagesToRemove) { + "Remove $package" | Out-Default -Transcript | Out-Null + } + } else { + Write-Host "No packages marked for deletion" + } + + $packagesToInstall = @{} + + foreach ($tier in $tiers) { + #Write-host "$logLead : Tier $tier Installs" + $installs = @() + + foreach ($package in $packagesToUpgrade) { + if ($package.Tier -eq $tier) { + $installs += $package + } + } + + foreach ($package in $newPackagesToInstall) { + if ($package.Tier -eq $tier) { + $installs += $package + } + } + + $packagesToInstall[$tier] = $installs + } + + Write-Host "$logLead : Setting Alkami services recovery to take no action" + Set-SDKServiceRecovery -ServiceName 'All' -Action 'TakeNoAction' + + Write-Host "$logLead : Stopping IIS and only services to be upgraded" + + # Only stop services we are going to be installing so we do as little interruption as possible + # Only has to be packages that are being upgraded, the ones being installed don't affect anything + foreach ($package in $packagesToUpgrade) { + $serviceInfo = (Get-ServiceInfoByCIMFragment -Fragment $package.Id) + if ($null -eq $serviceInfo) { + # This isn't a service, so we don't care + continue + } + $serviceName = $serviceInfo.Name + if (![string]::IsNullOrWhiteSpace($serviceName)) { + #Stop-AlkamiService -ServiceName $serviceName + #Write-Host "Package Id: " $package.Id + #Write-Host "ServiceName: " $serviceName + $service = Get-Service -Name $serviceName | Select-Object -First 1 + $processes = @(Get-ProcessFromService $service) + if ($processes.Count -ne 0) { + $process = ($processes | Select-Object -First 1) + "Stopping Process: $($process.Name) | $($process.id)" | Out-Default -Transcript | Out-Null + #Stop-ProcessIfFound $process.Name + Stop-Process $process.Id -force | Out-Null + } else { + "No running processes found for $($service.Name)" | Out-Default -Transcript | Out-Null + } + } + if ($null -eq $package.ComponentType) { + # because of legacy installers, remove this package's service registration to allow for downgrade experiences + Invoke-SCExe @('delete', $serviceName) + } + } + + if ($packagesToRemove.Count -gt 0) { + # remove packages we are uninstalling + foreach ($package in $packagesToRemove) { + Write-Verbose "Package to remove: $package" + $serviceName = (Get-ServiceInfoByCIMFragment -Fragment $package).Name + if (![string]::IsNullOrWhiteSpace($serviceName)) { + Stop-AlkamiService -ServiceName $serviceName + } + } + } + + Stop-IISOnly + + #Stop Radium if installed + $serviceName = Get-ServiceInfoByCIMFragment -QueryFragment (Join-Path (Get-OrbPath) 'Radium') + if (-not [string]::IsNullOrWhiteSpace($serviceName.Name)) { + Stop-AlkamiService -ServiceName $serviceName.Name + } + + #Stop Nag if installed + $serviceName = Get-ServiceInfoByCIMFragment -QueryFragment (Join-Path (Get-OrbPath) 'Nag') + if (-not [string]::IsNullOrWhiteSpace($serviceName.Name)) { + Stop-AlkamiService -ServiceName $serviceName.Name + } + # We are installing ORB, so we are always going to start it back up + $startIIS = $true + + if ($packagesToRemove.Count -gt 0) { + Write-Host "$logLead : Removing packages now" + # remove packages we are uninstalling + foreach ($package in $packagesToRemove) { + "Completely removed package [$package]" | Out-Default -Transcript | Out-Null + choco uninstall $package -n -f -i + } + } + + $chocoPath = Get-ChocolateyInstallPath + + Write-Host "$logLead : Installing packages now" + + "Dumping `$newPackagesToInstall to Transcript" | Out-Default -Transcript | Out-Null + (ConvertTo-Json $newPackagesToInstall) | Out-Default -Transcript | Out-Null + "Dumping `$packagesToUpgrade to Transcript" | Out-Default -Transcript | Out-Null + (ConvertTo-Json $packagesToUpgrade) | Out-Default -Transcript | Out-Null + "Dumping `$packagesToRemove to Transcript" | Out-Default -Transcript | Out-Null + (ConvertTo-Json $packagesToRemove) | Out-Default -Transcript | Out-Null + "Dumping `$tiers to Transcript" | Out-Default -Transcript | Out-Null + (ConvertTo-Json $tiers) | Out-Default -Transcript | Out-Null + + + foreach ($tier in $tiers) { + if ($tier -eq 1) { + Write-Host "Force removing AlkamiModules that just got installed" + Get-Module Alkami* | Remove-Module -Force + Import-Module Alkami.PowerShell.Database # ensure that database runners can get run, or show us an error message + #$tiers[0].Where({$_.ComponentType -eq 'SREModule' -and $_.Id -notmatch "\.SRE\."}).Id | Foreach-Object { Import-Module $_ } + + Write-Host "$logLead : Starting Redis" + Start-Service -Name "redis-master18620" + Start-Service -Name "redis-slave18621" + Write-Warning "$logLead : If the following two commands (Import-Module WebAdministration, Load-Assembly) fail and stop the installation, please close and reopen your powershell session and try again." + Write-Warning "$logLead : Alkami developers occasionally encounter this on new machine installs, and are working to resolve the issue." + + Import-Module WebAdministration + try { + Add-Type -AssemblyName "Microsoft.Web.Administration, Version=7.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" + } + catch { + # Just in case we have IIS Express and IIS loaded (dev machines) + [System.Reflection.Assembly]::LoadFile("C:\Windows\system32\inetsrv\Microsoft.Web.Administration.dll") + } + Assert-PlatformDeveloperKitComponentsInstalled + + # Before we install anything after the basics, we need to initialize the databases if they haven't been. We should have already delivered the database component. + if ($null -eq (Get-EnvironmentVariable -Name "Alkami_SDK_Initialization_DatabaseStandup")) { + Write-Host "Ensure default AlkamiDatabases are available and have been configured" + $connectionString = Get-MasterConnectionString + # names are hard-coded especially + Initialize-AlkamiDatabase $connectionString 'AlkamiMaster' + Initialize-AlkamiDatabase $connectionString 'DeveloperDynamic' + # TODO: Incredibly brittle. Will need to be refactored + # Easiest refactor is to restore a masterdb in the initialize functions above + C:\programdata\Alkami\Alkami\MachineSetup\DatabaseCore\Migrate.exe -a C:\programdata\Alkami\Alkami\MachineSetup\DatabaseCore\Alkami.Tools.MasterDatabaseMigration.dll --connectionString $connectionString -provider SqlServer2008 -tag alkamimaster -context sdk + Write-Host "Setting tenant" + Import-DeveloperDynamicTenant $connectionString + Set-EnvironmentVariable -Name "Alkami_SDK_Initialization_DatabaseStandup" -Value (Get-Date) -StoreName Machine + } + + # Before we install anything after the basics, we need to initialize the websites and webapps if they haven't been. We should have already delivered the required orb file components. + Write-Host "$logLead : Creating web sites and applications now" + Install-SDKIISComponents + + # You have to create the sites before you can run the migrations because of how the database users get setup in the database + # It's a pain, I know, but yay out of order operations ... + Write-host "$logLead : Running migrations" + Invoke-SDKAlkamiMigrations + } + + $packages = $packagesToInstall[$tier] + if (Test-IsCollectionNullOrEmpty $packages) { + Write-Host "$logLead : PackagesToInstall was empty" + continue + } else { + Write-Host "$logLead : Installing Tier $tier packages with count $($packages.count)" + } + $potentialServicePaths = @() + $parallelInstall = @() + # We have to check $allPackages because they may already be installed + foreach ($package in $packages) { + $packageFolder = Join-Path -Path (Join-Path -Path $chocoPath -ChildPath "lib") -ChildPath $package.Id + $potentialServicePaths += $packageFolder + } + ($potentialServicePaths -Join "`n") | Out-Default -Transcript | Out-Null + + foreach ($package in $packages) { + $packageFolder = Join-Path -Path (Join-Path -Path $chocoPath -ChildPath "lib") -ChildPath $package.Id + $runScripts = "-y" + if ($package.HasManifest -and $null -ne $package.ComponentType) { + $runScripts = "--skip-scripts" + $parallelInstall += $packageFolder + } + + $runQuietly = "--no-progress" + $sayVeryLittle = "--limit-output" + $ignoreDependencies = "-i" + if ($package.Id -in @("redis-64")) { + $ignoreDependencies = " " + } + + $version = "$($package.Version)" + $packageVersion = "[$($package.Version)]" + + $chocoSplat = @( + "upgrade" + $package.Id + $ignoreDependencies + $runScripts + $runQuietly + $sayVeryLittle + ) + + if ($package.VersionCanTakeAny) { + $version = "" + $packageVersion = "the latest available" + + # Write-Host "choco upgrade $($package.Id) -i $runScripts" + # choco upgrade $package.Id -i $runScripts #Removed -f + } else { + # Write-Host "choco upgrade $($package.Id) --version $version -i $runScripts" + # choco upgrade $package.Id --version $version -i $runScripts #Removed -f + $chocoSplat += "--version=$version" + } + + Write-Host "Downloading$(if (!$package.HasManifest){" and installing"}) $($package.id) - This may take a moment, even if things seem frozen." + # https://stackoverflow.com/questions/38523369/write-host-vs-write-information-in-powershell-5 + # Out-Default -Transcript <-- magic + Invoke-CallOperatorWithPathAndParameters -Path Choco -Arguments $chocoSplat *>&1 | Out-Default -Transcript | Out-Null + "Installed package [$($package.Id)] version to $packageVersion" | Out-Default -Transcript | Out-Null + } + + if (!(Test-IsCollectionNullOrEmpty $parallelInstall)) { + Write-Host "Running parallel installs. This is going to look like things are locked up. They aren't. Promise." + Invoke-Parallel -Objects $parallelInstall -ReturnObjects -Script { + param ($innerPackagePath) + $path = Join-Path -Path (Join-Path -Path $innerPackagePath -ChildPath tools) -ChildPath "chocolateyInstall.ps1" + if (Test-Path -Path $path) { + Write-Host "Calling $path" + & $path + } + } + } + $servicesInstalled = Invoke-Parallel -Objects $potentialServicePaths -ReturnObjects -Script { + param ($innerPackagePath) + $cimService = Get-ServiceInfoByCIMFragment -QueryFragment $innerPackagePath + if ($null -ne $cimService) { + return $cimService.Name + } + } + if ($tier -lt 3) { + foreach ($serviceName in $servicesInstalled) { + Write-Host "$logLead : Start-AlkamiService -ServiceName $serviceName" + Start-AlkamiService -ServiceName $serviceName + } + } else { + Invoke-Parallel -Objects $servicesInstalled -ReturnObjects -Script { + param ($serviceName) + Start-AlkamiService -ServiceName $serviceName + } + } + } + + # We've already done the database migrations, but we need to ensure the database users got setup for componentized web services + # Ensure logins are setup + Invoke-DatabaseConfigurationAlkamiMasterTask + Invoke-DatabaseConfigurationAlkamiTenantTask + + if ($startIIS) { + Remove-DotNetTemporaryFiles + Start-IISAndServices # restarts all of the services that were not upgraded/installed but stopped by installing Alkami.SRE.MigrationUtility + } + + Write-Host "$logLead : Setting Alkami services back to recover" + Set-SDKServiceRecovery -ServiceName 'All' -Action 'recovery' + Set-SDKServiceStartupType -ServiceName 'All' -StartupType 'Automatic' + } finally { + Stop-Transcript + Write-Host "`n`n`tTranscript log saved to $transcriptPath`n`n" + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiMasterTask.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiMasterTask.ps1 new file mode 100644 index 0000000..ecf717e --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiMasterTask.ps1 @@ -0,0 +1,12 @@ +function Invoke-DatabaseConfigurationAlkamiMasterTask { + [CmdletBinding()] + param () + + $connectionString = Get-MasterConnectionString + + if (Test-DatabaseExists $connectionString $databaseName) { + Write-Host "Database exists: ensuring users exist on the database" + Add-LocalServiceAccountsToDatabaseServer -ConnectionString ($connectionString -replace 'AlkamiMaster','master') + Add-LocalServiceAccountsToAlkamiDatabase -ConnectionString $connectionString -DatabaseName 'AlkamiMaster' + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiTenantTask.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiTenantTask.ps1 new file mode 100644 index 0000000..72fcaa8 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseConfigurationAlkamiTenantTask.ps1 @@ -0,0 +1,27 @@ +function Invoke-DatabaseConfigurationAlkamiTenantTask { + [CmdletBinding()] + param ( + [string[]]$TenantGuid + ) + + $logLead = Get-LogLeadName + + $connectionString = Get-MasterConnectionString + + # Get tenants for adding service accounts + $fullTenants = (Get-FullTenantListFromServer $connectionString) + $tenants = $fullTenants + + Write-Host "$logLead : get tenants" + + if (-not (Test-IsCollectionNullOrEmpty -Collection $TenantGuid)) { + Write-Host "$logLead : filter tenants because tenants were passed in" + $tenants = $fullTenants.Where({ $_.BankGuid.ToString() -in $TenantGuid }) + } + + # Add the service accounts + foreach ($tenant in $tenants) { + Write-Host "Adding local service accounts for `[$($tenant.Name) $($tenant.Signature)`]" + Add-LocalServiceAccountsToAlkamiDatabase -ConnectionString $tenant.ConnectionString -DatabaseName $tenant.Catalog + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiMasterTask.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiMasterTask.ps1 new file mode 100644 index 0000000..6e30260 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiMasterTask.ps1 @@ -0,0 +1,24 @@ +function Invoke-DatabaseMigrationAlkamiMasterTask { + param ( + $connectionString + ) + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + Write-Warning "!! THIS FEATURE WILL BE DEPRECATED. PLEASE CONTACT SDK TEAM TO REMOVE !!" + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + $databaseName = "AlkamiMaster"; + + $migrationPath = (Join-Path (Get-MigrationRunnerPath) "Alkami.Tools.MasterDatabaseMigration.dll"); + + if (!$connectionString) { + $connectionString = (Get-FormattedConnectionString '.' $databaseName); + } + + if ($connectionString -match 'localhost') { + if (!(Test-DatabaseExists $connectionString $databaseName)) { + $consume = (Initialize-AlkamiDatabase $connectionString $databaseName) + } + } + + Invoke-Migrate $connectionString $databaseName $migrationPath "sdk"; +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiTenantTask.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiTenantTask.ps1 new file mode 100644 index 0000000..13cea6f --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-DatabaseMigrationAlkamiTenantTask.ps1 @@ -0,0 +1,54 @@ +function Invoke-DatabaseMigrationAlkamiTenantTask { + param ( + $connectionString, + $hasTenant + ) + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + Write-Warning "!! THIS FEATURE WILL BE DEPRECATED. PLEASE CONTACT SDK TEAM TO REMOVE !!" + Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + + $migrationPath = (Join-Path (Get-MigrationRunnerPath) "Alkami.Tools.TenantMigration.dll") + + if ($hasTenant) { + if (!(Confirm-DatabaseAccess $connectionString)) { + throw "could not connect to $connectionString" + } + + Write-Host "Migrating database for `[$connectionString`]" + Invoke-Migrate $connectionString "" $migrationPath "SetupDynamicDbForSDK" + } else { + $databaseName = "AlkamiMaster" + + if (!$connectionString) { + $connectionString = (Get-FormattedConnectionString '.' $databaseName) + } + + ## Ensure the local tenant exists just in case it doesn't yet. + Import-DeveloperDynamicTenant $connectionString + + if (!(Test-DatabaseExists $connectionString "DeveloperDynamic")) { + $consume = Initialize-AlkamiDatabase $connectionString "DeveloperDynamic" + } + + ## Get tenants for migrating + $fullTenants = (Get-FullTenantListFromServer $connectionString) + $tenants = $fullTenants + + Write-Host "get tenants" + $tenantWhitelist += @((Get-DeveloperTenant).BankGuid.ToString()) + + $chocolateyParameters = @($chocolateyParameters,@() -ne $null)[0] + $tenantWhitelist += @($chocolateyParameters["TenantWhitelist"] -split ',') + + if ($tenantWhitelist.Count -gt 0) { + $tenants = $fullTenants | ? { $tenantWhitelist.Contains($_.BankGuid.ToString()) } + } + + ## Run the migrations + $tenants | % { + $tenant = $_ + Write-Host "Migrating database for `[$($tenant.Name) $($tenant.Signature)`]" + Invoke-Migrate $tenant.ConnectionString $tenant.Catalog $migrationPath "SetupDynamicDbForSDK" + } + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKAlkamiMigrations.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKAlkamiMigrations.ps1 new file mode 100644 index 0000000..6fd8dc9 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKAlkamiMigrations.ps1 @@ -0,0 +1,26 @@ +function Invoke-SDKAlkamiMigrations { + [CmdletBinding()] + param( + [string[]]$TenantGuid, + [string]$Path = "C:\ProgramData\Alkami\Alkami\MachineSetup\DatabaseCore" + ) + + # Ensure logins are setup + Invoke-DatabaseConfigurationAlkamiMasterTask + Invoke-DatabaseConfigurationAlkamiTenantTask -TenantGuid $TenantGuid + + # Ensure database compatability level has been set + try { + Invoke-SDKSetCompatibilityLevelAllLocalTenants + } catch {} + + $splat = @{ + MigrationTypeName = "orb" + MigrationRunnerPath = (Get-MigrationRunnerExe -runtime Framework) + OrbMigrateFolderPath = $Path + ConnectionString = (Get-MasterConnectionString) + Tags = "" # This used to be supplied as the individual database name. That seems unuseful + LegacyFluentMigratorContext = 'SetupDynamicDbForSDK' + } + Invoke-AlkamiMigrationRunner @splat +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKSetCompatibilityLevelAllLocalTenants.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKSetCompatibilityLevelAllLocalTenants.ps1 new file mode 100644 index 0000000..a1aab03 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Invoke-SDKSetCompatibilityLevelAllLocalTenants.ps1 @@ -0,0 +1,78 @@ +function Invoke-SDKSetCompatibilityLevelAllLocalTenants { + [CmdletBinding()] + param () + + $logLead = Get-LogLeadName + + $alkamiMasterConnectionString = (Get-ConnectionString 'AlkamiMaster') + + $tenants = Get-FullTenantListFromServer -ConnectionString $alkamiMasterConnectionString + + $compatabilityMatrix = @{} + $compatabilitySql = @" +SELECT name, compatibility_level +FROM sys.databases +WHERE compatibility_level < 140 +"@ + + # get the full set of compatability levels for the local environment only + $anyRecord = $false +#region My kingdom for a using statement syntax like C# + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $alkamiMasterConnectionString + $sqlConnection.Open() + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = $compatabilitySql + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + while ($reader.Read()) { + $compatabilityMatrix[$reader[0]] = $reader[1] + $anyRecord = $true + } + $reader.Dispose() + $sqlConnection.Close() +#endregion My kingdom for a using statement syntax like C# + + if (!$anyRecord) { + Write-Host "$logLead : All local databases at the minimum expected compatibility_level" + return + } + + # We can't just update all of the values to be what we want + # We HAVE to execute on ONLY the local databases that are related to Alkami (which means they are in the tenant table) + + $databasesToUpdate = @() + foreach ($tenant in $tenants) { + $sqlconn = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $tenant.ConnectionString + $catalog = $sqlconn.InitialCatalog + if ([string]::IsNullOrWhiteSpace($catalog)) { + $catalog = $tenant.Catalog + } + + if ($null -ne $compatabilityMatrix[$catalog]) { + # database is clearly local, and the value is below the threshold + $databasesToUpdate += $catalog + } + } + + # Also remember to check the AlkamiMaster connection strings + if ($null -ne $compatabilityMatrix[$sqlConnection.Database]) { + $databasesToUpdate += $sqlConnection.Database + } + + if ($databasesToUpdate.Count -eq 0) { + Write-Host "$logLead : No local databases found to update compatibilit_level for" + return + } + $sqlConnection.Open() + foreach ($database in $databasesToUpdate) { + $query = @" +USE master; +ALTER DATABASE [$database] SET COMPATIBILITY_LEVEL = 140; +"@ + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = $query + $consume = $command.ExecuteNonQuery() + Write-Host "$logLead : Updated compatibility_level for [$database] to 140" + } + $sqlConnection.Close() + $sqlConnection.Dispose() +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/New-SDKMachineSetup.ps1 b/Modules/Alkami.PowerShell.SDK/Public/New-SDKMachineSetup.ps1 new file mode 100644 index 0000000..94b849a --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/New-SDKMachineSetup.ps1 @@ -0,0 +1,64 @@ +Function New-SDKMachineSetup() { +<# +.SYNOPSIS +A function that encapsulates the setup of a new machine and completes the operations found on https://confluence.alkami.com/display/SDKC/Hardware+and+Software+Requirements + +.DESCRIPTION +Validates installation of required software, add feeds for nuget and chocolatey, exclude port ranges and setup the Alkami listeners. + +.EXAMPLE +Clients: New-SDKMachineSetup +Alkamists: New-SDKMachineSetup -Alkamist +Alkamists (Daily Builds): New-SDKMachineSetup -Alkamist -Daily + +.NOTES +General notes +#> + [CmdletBinding()] + param + ( + [Parameter(Mandatory=$true)] + [string]$Username, + [Parameter(Mandatory=$true)] + [string]$Password, + [Parameter(Mandatory=$false)] + [switch]$Alkamist, + [Parameter(Mandatory=$false)] + [switch]$Daily + ) + + # Confirm the required software is installed + Confirm-Software + + $feed = "feeds" + + if ($Alkamist) { + $feed = "packagerepo" + } + + # Add Alkami's NuGet sources + Write-Host "Adding Alkami Dev NuGet source" + nuget sources add -name "Alkami Dev" -source "https://$feed.alkamitech.com/nuget/nuget.dev" -user $Username -password $Password + Write-Host "Adding Alkami Third-Party NuGet source" + nuget sources add -name "Alkami Third-Party" -source "https://$feed.alkamitech.com/nuget/ThirdParty" -user $Username -password $Password + + # Then add our Chocolatey Dev feed as a choco source, passwords with special characters will need to be surrounded in "'mypassword'" double and single quotes + Write-Host "Adding Alkami Choco.Dev Chocolatey source" + choco source add -n=AlkamiChocoDev -s "'https://$feed.alkamitech.com/nuget/choco.dev'" -u="'$Username'" -p="'$Password'" + + if ($Alkamist) { + Write-Host "Adding Alkami Choco.SDK Chocolatey source" + choco source add -n=AlkamiChocoSdk -s "'https://$feed.alkamitech.com/nuget/choco.sdk'" -u="'$Username'" -p="'$Password'" + if (-not $Daily) { + choco source disable -n "AlkamiChocoSdk" + } + } + + $output = $false + # Note: Run below 2 commands to reserve specific ports. + $output = Add-NetshExcludedPortRange -StartPort 12345 -Range 2 + $output = Add-NetshExcludedPortRange -StartPort 50000 -Range 30 + + # Ensure the specifically requested Alkami listeners are added to the Windows netsh repository + $output = Set-DefaultNetshIPListens +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/New-SDKSnapshot.ps1 b/Modules/Alkami.PowerShell.SDK/Public/New-SDKSnapshot.ps1 new file mode 100644 index 0000000..700bffb --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/New-SDKSnapshot.ps1 @@ -0,0 +1,111 @@ +function New-SDKSnapshot { +<# +.SYNOPSIS + Capture the current Chocolatey package state of the local machine environment + +.DESCRIPTION + This will create a Chocolatey .nuspec package manifest in the output directory passed by argument (defaults to "c:\alkamisdk\snapshots"). The metadata section will be largely empty, each team should fill in as necessary and then commit to their source code repository and keep the package fresh by manually adjusting the contents or using the snapshot command.� + +.PARAMETER Name + The name of the .nuspec file generated by this snapshot command. The package name will default to "alkami.machinesetup.sdk.[guid]" where [guid] is a randomly generated guid value. + +.PARAMETER Path + The path defaults to "c:\alkamisdk\snapshots" if none is passed in + +.OUTPUTS +A Chocolatey package manifest. + +.NOTES +General notes +#> + [CmdletBinding()] +param( + [string] $Name, + [string] $Path + ) + if(!$Path){ + $Path = "C:\alkamisdk\snapshots" + } + + if(!$Name){ + $guid = (New-Guid).Guid; + $Name = "alkami-machinesetup-sdk-snapshot-$guid" + } + + $snapshotPath = Join-Path $Path $Name; + + $chocolateyPackages = & choco list -lo "Alkami" + + $nuspecTemplate = $nuspecTemplate.replace("[[id]]", $Name); + $nuspecTemplate = $nuspecTemplate.replace("[[title]]", $Name + " (install)"); + + $nuspecXmlDoc = [Xml]($nuspecTemplate); + $depsEle = $nuspecXmlDoc.GetElementsByTagName("dependencies"); + + + foreach($package in $chocolateyPackages) { + + $packageName = $package.Split(' ')[0]; + $version = $package.Split(' ')[1]; + $append = $true; + if($packageName -match "Alkami"){ + + $versionAttribute = $nuspecXmlDoc.CreateAttribute("version"); + $nameAttribute = $nuspecXmlDoc.CreateAttribute("id"); + + $nameAttribute.Value = $packageName; + $versionAttribute.Value = $version; + + if($packageName -match "Alkami.MachineSetup"){ + if($packageName -eq "Alkami.MachineSetup.SDK") { + $versionAttribute.Value = "[$version]"; + $append = $false; + } + else { + continue; + } + } + + $dependencyElement = $nuspecXmlDoc.CreateElement("dependency", "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"); + $consume = $dependencyElement.SetAttributeNode($nameAttribute); + $consume = $dependencyElement.SetAttributeNode($versionAttribute); + + if($append) { + + $consume = $depsEle.AppendChild($dependencyElement); + } + else + { + $consume = $depsEle.PrependChild($dependencyElement); + } + } + } + + if(!(Test-Path $Path)) { + New-Item -Path $Path -ItemType Directory + } + $consume = $nuspecXmlDoc.Save((Join-Path $Path "$Name.nuspec")); +} + +$nuspecTemplate = @" + + + + [[id]] + `$version$ + [[title]] + Alkami Technology, Inc. + Alkami Technology, Inc. + http://alkami.com/files/orblicense.html + https://confluence.alkami.com + https://www.alkami.com/files/alkamilogo75x75.png + false + This package was created from a snapshot of an SDK environment + + Copyright (c) $($(get-date).year) Alkami Technology, Inc. + MachineSetup SDK TeamPackage + + + + +"@ \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Open-SDKDoc.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Open-SDKDoc.ps1 new file mode 100644 index 0000000..06d6d55 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Open-SDKDoc.ps1 @@ -0,0 +1,30 @@ +function Open-SDKDoc { + <# + .SYNOPSIS + View this modules documentation in the browser. + + .DESCRIPTION + This function opens the HTML documentation for this modules present version. + + .INPUTS + None + + .OUTPUTS + None + + .EXAMPLE + Open-SDKDoc + + .NOTES + General notes + #> + [CmdletBinding()] + param( + ) + process { + # Start-Process "C:\programdata\chocolatey\lib\Alkami.PowerShell.SDk\doc\www\index.html"; + Write-Host "Use PowerShell's Get-Help function to explore the module's commands." + (get-module alkami.powershell.sdk).ExportedCommands | % {$_} + } + } + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-HostsFileEntry.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-HostsFileEntry.ps1 new file mode 100644 index 0000000..4f9d22d --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-HostsFileEntry.ps1 @@ -0,0 +1,67 @@ +function Remove-HostsFileEntry { +<# +.SYNOPSIS + Returns all hosts file entries as a list of objects of the format: + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.PARAMETER IpAddress + The IP Address of the relevant hosts entry + +.PARAMETER Hostname + The hostname of the relevant hosts entry + +.PARAMETER Force + Because some records are marked with the word keep to not be deleted + +.OUTPUTS + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [switch]$Force + ) + + $logLead = Get-LogLeadName + + $records = Get-HostsFileAllRecords + + $removedRecords = $records.Where({($_.IpAddress -eq $IpAddress) -and ($_.Hostname -eq $Hostname)}) + foreach ($record in $removedRecords) { + Write-Host "$logLead : Removing: $(Format-HostsFileRecord -Record $record)" + } + $updatedRecords = $records.Where({!(($_.IpAddress -eq $IpAddress) -and ($_.Hostname -eq $Hostname))}) + + $keepRecords = $removedRecords.Where({$_.Keep}) + if (!(Test-IsCollectionNullOrEmpty $keepRecords)) { + if ($Force) { + Write-Warning "$logLead : Skipping the following keep records" + } else { + Write-Warning "$logLead : Found Keep records in the remove-requested records. Re-adding to the updated records list. Use -Force to override." + } + $updatedKeepRecords = @() + foreach ($record in $keepRecords) { + $formattedRecord = Format-HostsFileRecord -Record $record + if ($Force) { + Write-Host "$logLead : Removing, not keeping: [$formattedRecord]" + } else { + Write-Host "$logLead : Keeping: [$formattedRecord]" + $newRecord = @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $formattedRecord; BlankLine = $false; } + $updatedKeepRecords += $newRecord + } + } + $updatedRecords += $updatedKeepRecords + } + + if ($removedRecords.Count -gt 0) { + Write-Host "$logLead : Removing: $removedRecordsCount records" + Save-CompleteHostsFile -Record $updatedRecords + } else { + Write-Warning "$logLead : No records found to remove" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-LegacyDatabaseUsers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-LegacyDatabaseUsers.ps1 new file mode 100644 index 0000000..f260f42 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-LegacyDatabaseUsers.ps1 @@ -0,0 +1,64 @@ +function Remove-LegacyDatabaseUsers { +<# +.SYNOPSIS + Remove the legacy database users (IIS App Pools) from the system + +.PARAMETER ConnectionString + The connection string of the database to cleanup + +.PARAMETER DbName + [Obsolete] The database name associated with this connection string +#> + [CmdletBinding()] + [OutputType([void])] + param ( + $ConnectionString, + $DbName + ) + + Confirm-DatabaseAccess $ConnectionString + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $ConnectionString + + # The original passed in value is now obsolete, just use the one on the connection string now + $DbName = $sqlConnection.Database + + if($DbName -match 'AlkamiMaster' -or $DbName -match 'DeveloperDynamic' ) { + Write-Host "Cleaning crusty users from connection string: " $ConnectionString + } else { + # Only act on local Alkami databases + return + } + + $sqlConnection.Open() + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + $command.CommandText = "select [name] from [sys].[database_principals] where [type]='u' and [name]!='dbo';" + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader() + $DbNames = @() + while ($reader.Read()) { + $DbNames += $reader[0].ToString() + } + $reader.Dispose() + + # TODO: Should we death all users in AlkamiMaster and DeveloperDynamic no matter who they are? + foreach ($account in (Get-SDKUserMatrix)) { + # This will get rid of any IIS Users in the database + # This does not get rid of the domain users in the database + $username = $account.Username.Trim() + if ($DbNames.Contains($username)) { + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand() + + if($DbName -match 'AlkamiMaster' -or $DbName -match 'DeveloperDynamic' ) { + $command.CommandText = "DROP USER [$username];" + } + else { + $command.CommandText = "DROP LOGIN [$username];" + } + + $command.ExecuteNonQuery() | Out-Null + $command.Dispose() + } + } + $sqlConnection.Dispose() +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-OrbHostEntries.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-OrbHostEntries.ps1 new file mode 100644 index 0000000..ce3e0fb --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-OrbHostEntries.ps1 @@ -0,0 +1,20 @@ +function Remove-OrbHostEntries { +<# +.SYNOPSIS + Removes all the known ORB entries from the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + [OutputType([void])] + param() + + $logLead = Get-LogLeadName + + $knownHostEntries = (Get-KnownDeveloperHostsEntries) + foreach ($entry in $knownHostEntries) { + Write-Host "$logLead : Removing hosts file entry with IpAddress $($entry.IpAddress) and Hostname $($entry.Hostname)" + Remove-HostsFileEntry -IpAddress $entry.IpAddress -Hostname $entry.Hostname + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-SDKServices.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-SDKServices.ps1 new file mode 100644 index 0000000..d7aefca --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-SDKServices.ps1 @@ -0,0 +1,28 @@ +function Remove-SDKServices { +<# +.SYNOPSIS +A quick function to remove any Windows service from the services registry where the name contains "Alkami" + +.DESCRIPTION +Run this to remove the Alkami services when resetting the development environment + +.EXAMPLE +Remove-SDKServices + +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param( + ) + $continuePrompt = "Are you sure you want to remove all Alkami Windows Services?" + $yesToAll = $false + $noToAll = $false + $shouldContinue = $PSCmdlet.ShouldContinue($continuePrompt, 'Confirm', ([ref] $yesToAll), ([ref] $noToAll)) + if (!$shouldContinue) { + return + } + + $serviceNames = (Get-ServiceInfoByCIMFragment C:\programdata\chocolatey\lib\).Where({$_.Name -match 'Alkami' -and $_.Name -ne "Alkami.Sidekick.Client"}).Name + foreach ($service in $serviceNames) { + Invoke-SCExe @('delete', $service) + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-SMSvcHostBlankSecurityIdentifiers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-SMSvcHostBlankSecurityIdentifiers.ps1 new file mode 100644 index 0000000..0ee6573 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-SMSvcHostBlankSecurityIdentifiers.ps1 @@ -0,0 +1,27 @@ +function Remove-SMSvcHostBlankSecurityIdentifiers { +<# +.SYNOPSIS + Remove blank entries that should have a SID in them +#> + [CmdletBinding()] + param () + + $paths = @( + "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SMSvcHost.exe.config" + "C:\Windows\Microsoft.NET\Framework\v4.0.30319\SMSvcHost.exe.config" + ) + + foreach ($path in $paths) { + $configuration = [xml](Get-Content -Path $path) + + # we only need to remove the ones that are blank + $nodes = @($configuration.configuration.'system.serviceModel.activation'.'net.tcp'.allowAccounts.add).Where({[string]::IsNullOrWhiteSpace($_.securityIdentifier)}) + + foreach ($node in $nodes) { + # not really anything to show here since they are blank sids + $node.ParentNode.RemoveChild($node) + } + + $configuration.Save($path) + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Remove-Sdk.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Remove-Sdk.ps1 new file mode 100644 index 0000000..43254a1 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Remove-Sdk.ps1 @@ -0,0 +1,195 @@ +function Stop-AlkamiServicesForDeletetion { + + [System.Collections.ArrayList]$stopLast = "Alkami.MicroServices.Broker.Host", "Alkami.Services.Subscriptions.Host"; + + + Get-Service alkami* | Where-Object { $_.Status -eq "Running" } | ForEach-Object { Set-service -Name $_.Name -StartupType "manual" } + #stop everyhing but broker sub service + (Get-Service alkami*) | ForEach-Object { + if ( $stopLast -contains $_.Name) { + #do nothing + } + else { + Stop-AlkamiService -ServiceName $_.Name + } + } + + #stop broker and sub service + (Get-Service alkami*) | ForEach-Object { + if ( $stopLast -contains $_.Name) { + Stop-AlkamiService -ServiceName $_.Name + } + else { + #donothing + } + } +} + +function Remove-AlkamiChocoPackageFolders { + $ChocoInstallPath = "C:\ProgramData\chocolatey\lib\"; + # $ChocoInstallPath = "C:\test\"; + Get-ChildItem -Path $ChocoInstallPath Alkami* | ForEach-Object { + $PathToRemove = $ChocoInstallPath + $_; + Write-Host removeing $ChocoInstallPath$_; + remove-item -Path $PathToRemove -Recurse -Force; + } +} + +function Remove-OrbFolder { + Write-Host "removing orb folder" + $OrbFilePath = "C:\Orb"; + remove-item -Path $OrbFilePath -Recurse -Force; + +} +function Remove-ServicesFromRegistry { + $serviceNames = (Get-ServiceInfoByCIMFragment C:\programdata\chocolatey\lib\).Where({ $_.Name -match 'Alkami' -and $_.Name -ne "Alkami.Sidekick.Client" }).Name + foreach ($service in $serviceNames) { + Invoke-SCExe @('delete', $service) + } +} +#flags to add +#-sites remove IIS sites +#-appPools remove App pools added by alkami +#-devDb remove developer dynamic database +#-MainDb remove main database + +#add other flags/methods that group the above items like +#-full +#that will remove everything + +function Remove-SDK { + + [CmdletBinding()] + param ( + #Removes the developer dynamic database, will not remove stage match database + [switch] $DevDb, + + #Removes IIS dev and admin sites, does not remove the stage match site + [switch] $Sites, + + #Removes The Webclient App pool + [Switch] $AppPools, + + #combines the following flags into one $DevDb $Sites $AppPools + [Switch] $Hard + ) + Remove-SDKCore; + + + if ($DevDb) { + #remove developerDynamic database + remove-DevDynamicDB; + Write-Host "-----------------"; + } + + if ($Sites) { + #remove iis sites + + remove-DeveloperDevIisSites; + Write-Host "-----------------"; + } + if ($AppPools) { + remove-WebCLientAppPool + Write-Host "-----------------"; + #remove app pools + } + + if ($Hard) { + #Hard reset + remove-DevDynamicDB; + Write-Host "-----------------"; + remove-AlkamiMasterDB; + Write-Host "-----------------"; + remove-DeveloperDevIisSites; + Write-Host "-----------------"; + remove-WebCLientAppPool; + Write-Host "-----------------"; + remove-ChocoSources; + Write-Host "-----------------"; + remove-NugetSource; + Write-Host "-----------------"; + remove-portExclusions; + Write-Host "-----------------"; + } + Start-IISOnly; + +} + +function Remove-SDKCore { + + Stop-AlkamiServicesForDeletetion; + Write-Host "-----------------"; + Stop-IISOnly; + Write-Host "-----------------"; + Remove-AlkamiChocoPackageFolders; + Write-Host "-----------------"; + Remove-OrbFolder; + Write-Host "-----------------"; + Remove-ServicesFromRegistry; + Write-Host "-----------------"; +} +function remove-DevDynamicDB { + #alkami.machinesetup.sdk.database + Write-Host "removing DeveloperDynamic database"; + invoke-sqlcmd -Query "alter database DeveloperDynamic set single_user with rollback immediate; Drop database DeveloperDynamic;" +} +function remove-AlkamiMasterDB { + #alkami.machinesetup.sdk.database + Write-Host "removing AlkamiMaster database"; + invoke-sqlcmd -Query "alter database AlkamiMaster set single_user with rollback immediate; Drop database AlkamiMaster;" + Write-Host "removing Database Standup variable"; + Remove-EnvironmentVariable -Name "Alkami_SDK_Initialization_DatabaseStandup" -StoreName "Process" + Remove-EnvironmentVariable -Name "Alkami_SDK_Initialization_DatabaseStandup" -StoreName "Machine" +} +function remove-DeveloperDevIisSites { + + Write-Host "removing developer.dev.alkamitech.com"; + Remove-IISSite -Name "developer.dev.alkamitech.com"; + + Write-Host "removing admin-developer.dev.alkamitech.com"; + Remove-IISSite -Name "admin-developer.dev.alkamitech.com"; + + Write-Host -Name "legacy WebClient client" + Remove-IISSite -Name "WebClient"; + + Write-Host -Name "legacy WebClient client" + Remove-IISSite -Name "WebClientAdmin"; + + +} +function remove-portExclusions { + Write-Host -Name "remove port exclusions" + netsh int ipv4 delete excludedportrange protocol=tcp startport=12345 numberofports=2 + netsh int ipv4 delete excludedportrange protocol=tcp startport=50000 numberofports=30 +} +Function remove-WebCLientAppPool { + Write-Host "removing App Pools"; + Remove-WebAppPool -Name "WebClient" + Remove-WebAppPool -Name "Admin" + Remove-WebAppPool -Name "WebClientAdmin" + Remove-WebAppPool -Name "STSConfiguration" + Remove-WebAppPool -Name "SecurityManagementService" + Remove-WebAppPool -Name "RP-STS" + Remove-WebAppPool -Name "NotificationService" + Remove-WebAppPool -Name "NagConfigurationService" + Remove-WebAppPool -Name "MessageCenterService" + Remove-WebAppPool -Name "IPSTS" + Remove-WebAppPool -Name "DefaultAppPool" + Remove-WebAppPool -Name "CoreService" + Remove-WebAppPool -Name "ContentService" + Remove-WebAppPool -Name "AuditService" + Remove-WebAppPool -Name "BankService" +} + +Function remove-ChocoSources { + Write-Host "removing choco sources"; + choco source remove -n="choco.dev" + choco source remove -n="choco.internal" + choco source remove -n="AlkamiChocoDev" +} + +Function remove-NugetSource { + Write-Host "removing nuget source"; + dotnet nuget remove source "Alkami Third-Party" + dotnet nuget remove source "Alkami Dev" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Repair-AlkamiDeveloperLoginsAndStartServices.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Repair-AlkamiDeveloperLoginsAndStartServices.ps1 new file mode 100644 index 0000000..b75995d --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Repair-AlkamiDeveloperLoginsAndStartServices.ps1 @@ -0,0 +1,139 @@ +function Repair-AlkamiDeveloperLoginsAndStartServices { +<# +.SYNOPSIS + Repair the developer environment to start services and cleanup other things + +.DESCRIPTION + This command will do the following unless overridden + * Flush DNS cache via ipconfig + * Update group policy definitions to ensure you aren't missing AD concerns + * This can take a while to complete + * Ensures ACL are properly set on certificates + * Reset the Windows Performance Counter cache + * Clears the ASP.NET Temp Folder (under C:\Windows\Microsoft.NET) + * This will restart IIS, you may want to skip that step if you don't need to clear those files. + * Clearing those files causes WebClient to take much longer to start back up + * Stops the Windows Services, resets their gMSA facility, and restarts them + * Pings the WCF IIS services (such as BankService) to "warm the cache" + * Grant logon as a service rights + + Why does the "gMSA facility" need to be "reset"? + - This is because gMSA accounts like corp\dev.dbms$ are actually passworded accounts, + it's just a seamlessly shared password to your machine via Active Directory. + Those accounts can't be used for interactive login, but the credentials + can be used to communicate with AD governed resources, such as SQL Server, or + the use of network ports typically reserved for OS level (80, 443, etc). + Because it _does_ have a password, and because Alkami rotates passwords, + sometimes the "password" "stored" on your machine is stale, so AD will not + reauthenticate the service. + The functionality to "reset" the "gMSA facility" is maintained by SRE, so it + stays in line with the rest of Alkami's best-practices, and you should be able + to rely on this script being updated if SRE makes changes. + +.PARAMETER SkipFlushDNS + Skip flushing the DNS resolver cached entries and group-policy updates + +.PARAMETER SkipResetCounter + Skip resetting the Windows Performance Counter cache + +.PARAMETER SkipCertificates + Will not ensure ACLs on expected certificates + +.PARAMETER SkipClrAsp + Skip flushing the ASP Temp cache + +.PARAMETER SkipResetServices + Will not reset services (you probably wanted to do this exact function tho) + +.PARAMETER SkipPingServices + Will not ping services such as BankService to "warm the cache" + +.PARAMETER SkipGrantLogonRights + Will not grant logon rights to the default services +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [switch]$SkipFlushDNS, + [Parameter(Mandatory = $false)] + [switch]$SkipResetCounter, + [Parameter(Mandatory = $false)] + [switch]$SkipCertificates, + [Parameter(Mandatory = $false)] + [Alias('SkipASPNetTemps')] + [switch]$SkipClrAsp, + [Parameter(Mandatory = $false)] + [switch]$SkipResetServices, + [Parameter(Mandatory = $false)] + [switch]$SkipPingServices, + [Parameter(Mandatory = $false)] + [switch]$SkipGrantLogonRights + ) + + if (-not $SkipFlushDNS) { + Write-Host "Flushing DNS" + ipconfig /flushdns + Write-Host "Updating GroupPolicy" + gpupdate /force + } + + if (-not $SkipResetCounter) { + Write-Host "Resetting windows performance counters" + try{ + lodctr /r + } catch { + Write-Host "Reattempting to reset windows performance counters from the C:\ directory" + $whereWasI = Get-Location + Set-Location -Path C:\ + lodctr /r + $whereWasI | Set-Location + } + } + + if (-not $SkipCertificates) { + $usernames = (Get-SDKUserMatrix).Where({ $_.RequiresCertAccess -eq $true }).DomainUsername + if ($usernames -contains 'CORP\dev.dbms$') { + $usernames += 'CORP\dev.micro$' + } + Repair-SDKAlkamiDeveloperCertificatePermissions -PermittedIdentities $usernames + } + + if (-not $SkipClrAsp) { + iisreset /stop + Write-Host "Clearing asp.net temp files" + Remove-DotNetTemporaryFiles + iisreset /start + } + + if (-not $SkipGrantLogonRights) { + $usernames = (Get-SDKUserMatrix).DomainUsername + if ($usernames -contains 'CORP\dev.dbms$') { + $usernames += 'CORP\dev.micro$' + } + foreach ($username in $usernames) { + Grant-UserLogonAsServiceRights -Username $username + } + } + + if (-not $SkipResetServices) { + Stop-ServicesOnly + Clear-GMSAPasswords + $redisServices = Get-ServiceInfoByCIMFragment -Fragment "redis-" + foreach ($redisService in $redisServices) { + Start-AlkamiService $redisService.Name + } + Start-ServicesOnly + } + + if (-not $SkipPingServices) { + Write-Host "Pinging services" + try{ + Ping-AlkamiServices -skipCheck + #Ping-AlkamiWebSites + } catch { + Write-Host "Failed to ping services" + } + } +} + +Set-Alias -Name FixLogins -Value Repair-AlkamiDeveloperLoginsAndStartServices \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Repair-SDKAlkamiDeveloperCertificatePermissions.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Repair-SDKAlkamiDeveloperCertificatePermissions.ps1 new file mode 100644 index 0000000..4005bdb --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Repair-SDKAlkamiDeveloperCertificatePermissions.ps1 @@ -0,0 +1,41 @@ +function Repair-SDKAlkamiDeveloperCertificatePermissions { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$PermittedIdentities + ) + + $logLead = Get-LogLeadName + + $certs = Get-ChildItem Cert:\LocalMachine\my\ | Where-Object { $_.FriendlyName -match 'Alkami' } + $certGroups = $certs | Group-Object -Property FriendlyName + $shouldExit = $false + + foreach ($group in $certGroups) { + if ($group.Count -gt 1) { + Write-Warning "$logLead : You have too many certificates locally with the friendly name [$($group.Name)]" + $shouldExit = $true + } + } + + if ($shouldExit) { + return + } + + $expectedCerts = @() + $expectedCerts += Find-CertificateByName -CommonName "*.dev.alkamitech.com" -StoreLocation LocalMachine -StoreName My + $expectedCerts += Find-CertificateByName -CommonName "Alkami Issued Token" -StoreLocation LocalMachine -StoreName My + $expectedCerts += Find-CertificateByName -CommonName "Alkami RPSTS" -StoreLocation LocalMachine -StoreName My + $expectedCerts += Find-CertificateByName -CommonName "Alkami Mutual Client" -StoreLocation LocalMachine -StoreName My + $expectedCerts += Find-CertificateByName -CommonName (Get-FullyQualifiedServerName) -StoreLocation LocalMachine -StoreName My + $expectedCerts += Find-CertificateByName -CommonName "Alkami Mutual Service" -StoreLocation LocalMachine -StoreName My + + foreach ($cert in $expectedCerts) { + Write-Host "Updating [$($cert.FriendlyName)] for [$($PermittedIdentities)]" + foreach ($identity in $PermittedIdentities) { + Set-AclOnCert -Thumbprint $cert.Thumbprint -Identity $identity -FileSystemRights "FullControl" -Type "Allow" -StoreName "My" + Set-AclOnCert -Thumbprint $cert.Thumbprint -Identity $identity -FileSystemRights "FullControl" -Type "Allow" -StoreName "TrustedPeople" -ErrorAction SilentlyContinue + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Reset-SDKEnvironment.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Reset-SDKEnvironment.ps1 new file mode 100644 index 0000000..645ac65 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Reset-SDKEnvironment.ps1 @@ -0,0 +1,98 @@ +function Reset-SDKEnvironment { +<# +.SYNOPSIS + Run the command, answer the quetions, cross your fingers. + +.DESCRIPTION + The certificates are left behind. IIS is brittle and things may be funky, this wont always work in some edge cases. Running the same options more than once won't normally result in different behavior from the first time. + +.OUTPUTS + A few gigs of space. + +.EXAMPLE + $> Reset-SDKEnvironment + $> Remove Alkami C:\Orb directory? (y/n) + $> Remove Alkami IIS Applications? (y/n) + $> Remove Alkami Certificates? (y/n) + $> Remove Alkami Chocolatey packages? (y/n) + $> Remove Alkami Databases? (y/n) + $> Remove Alkami Microservices from Windows Service registry? (y/n) + +.EXAMPLE + $> Reset-SDKEnvironment -y + +.EXAMPLE + $> Reset-SDKEnvironment @('orb', 'iis', choco', 'database', 'services', 'certs'); + +.EXAMPLE + $> Reset-SDKEnvironment -Targets @('choco', 'services'); + +.NOTES +General notes +#> + [CmdletBinding()] + param( + [string[]] $Targets, + [switch]$Experimental + ) + if ($Experimental) { + Remove-EnvironmentVariable -Name 'Alkami_SDK_PostInstall_DatabaseConfiguration' -StoreName User + Remove-File -Path "C:\ProgramData\Alkami\SDK\local_installed_version.txt" + } + + if(!$Targets) { + + } + # https://powershelldoc.sre.alkami.net/Alkami.PowerShell.Common/Alkami.PowerShell.Common/Move-LogsAndDeleteDotNetTemps.html + # https://powershelldoc.sre.alkami.net/Alkami.PowerShell.Common/Alkami.PowerShell.Common/Remove-OldArchivedLogFiles.html + # https://powershelldoc.sre.alkami.net/Alkami.PowerShell.Common/Alkami.PowerShell.Common/Remove-ORBLogFiles.html + + Write-Host "Coming Soon!" + + if ($Experimental) { + Remove-OrbHostEntries + } +} + +Function resetHosts { + $hostsPath = "$env:windir\System32\drivers\etc\hosts"; + Get-Content -Path $hostsPath | Where-Object {$_ -contains "#"} | Out-File $hostsPath -append + +} + +Function resetMachineConfig { + $newContent = "" + foreach ($line in $content){ + + if($line.length -eq 0 -or $line.Substring(0,1) -eq "#"){ + $newContent = $newContent + $line + "`n" + } + + } + #Write-Host "New COntent $newContent" + Set-Content -Path $hostsPath -Value $newContent +} + +Function resetCerts { + +} + +Function resetChoco { + +} + +Function resetIIS { + +} + +Function resetDatabases { + +} + +Function resetServices { + +} + +Function resetOrb { + +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKServices.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKServices.ps1 new file mode 100644 index 0000000..2c5a4d0 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKServices.ps1 @@ -0,0 +1,33 @@ +function Restart-SDKServices { + <# + .SYNOPSIS + A quick way to restart all of Alkami's services, including Redis. + + .DESCRIPTION + This will make sure redis and Alkami services are stopped and then restarted in the proper order. + + .INPUTS + None + + .OUTPUTS + Object of users + + .EXAMPLE + Restart-SDKServices + + .NOTES + General notes + #> + [CmdletBinding()] + param( + + ) + Stop-IISAndServices; + #Write-Host "Restarting Windows Activation Service and Dependencies"; + #restart-service @("WAS") -Force; + Remove-DotNetTemporaryFiles; + Write-Host "Restarting redis-master18620 and redis-slave18621"; + restart-service @("redis-master18620", "redis-slave18621") -Force; + Start-IISAndServices; +} + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKWebClients.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKWebClients.ps1 new file mode 100644 index 0000000..dd67659 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Restart-SDKWebClients.ps1 @@ -0,0 +1,39 @@ +Function Restart-SDKWebClients { +<# +.SYNOPSIS +A function that will restart just the WebClient and WebAdmin IIS processes + +.DESCRIPTION +Does a surgical restart of the WebClient and Admin sites with the option of clearing the temporary ASP.NET files + +.EXAMPLE +Resart-SDKWebClients +Resart-SDKWebClients -ClearTemp (Clears Temp ASP.NET files) + +.NOTES +General notes +#> + [CmdletBinding()] + param + ( + [Parameter(Mandatory=$false)] + [switch]$ClearTemp + ) + $logLead = Get-LogLeadName + + $appPools = @("WebClientAdmin","Admin","WebClient") + + ForEach ($appPool in $appPools) { + Get-WmiObject -NameSpace 'root\WebAdministration' -class 'WorkerProcess'| + Where-Object {$_.AppPoolName -match $appPool} | + Select-Object -Expand ProcessId | + ForEach-Object { + Write-Host "$logLead : Restarting $appPool ..." + Stop-Process $_ -force | Out-Null + } + } + + if ($ClearTemp) { + Remove-DotNetTemporaryFiles + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Save-CompleteHostsFile.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Save-CompleteHostsFile.ps1 new file mode 100644 index 0000000..e3737a6 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Save-CompleteHostsFile.ps1 @@ -0,0 +1,37 @@ +function Save-CompleteHostsFile { +<# +.SYNOPSIS + Used to save a list of hosts records to the hosts file. Will overwrite all other information in the file. Use with discretion. + +.PARAMETER Records + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.PARAMETER RemoveDuplicates + Remove duplicates before saving +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [object[]]$Records, + [switch]$RemoveDuplicates + ) + + $logLead = Get-LogLeadName + + if (Test-IsCollectionNullOrEmpty $Records) { + throw "$logLead : no records found to write to the hosts file" + } + + if (($null -eq $Records.IpAddress) -or ($null -eq $Records.Hostname)) { + throw "$logLead : no records found with ipaddress or hostname, can not continue" + } + + if ($RemoveDuplicates) { + # Reuse variable is frowned upon + $Records = Remove-DuplicateHostsFileRecords -Record $Records + } + + $lines = Format-HostsFileRecord -Record $Records + + $lines | Out-File (Get-HostsFilePath) -Encoding ASCII +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Save-InstallVersions.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Save-InstallVersions.ps1 new file mode 100644 index 0000000..d5fa5e9 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Save-InstallVersions.ps1 @@ -0,0 +1,13 @@ +function Save-InstallVersions { + param ( + $Version, + $Features + ) + + $filePath = Get-SavedInstallVersionPath + $parentFolder = Split-Path $filePath -Parent + if (!(Test-Path -Path $parentFolder )) { + New-Item -ItemType Directory -Path $parentFolder -Force | Out-Null + } + Set-Content -Path $filePath -Value $Version,$Features | Out-Null +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-AclOnCert.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-AclOnCert.ps1 new file mode 100644 index 0000000..6d4c3b1 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-AclOnCert.ps1 @@ -0,0 +1,20 @@ +function Set-AclOnCert { + [CmdletBinding()] + param( + [psobject]$Thumbprint, + [string]$Identity, + [string]$FileSystemRights, + [string]$Type, + [string]$StoreName + ) + + $mycert = Get-Item -Path cert:\LocalMachine\$StoreName\$Thumbprint + $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $keyName = $mycert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyFullPath = $keyPath + $keyName + $acl = (Get-Item $keyFullPath).GetAccessControl("Access") + $permission=$Identity,$FileSystemRights,$Type + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + $acl.AddAccessRule($accessRule) + Set-Acl -Path $keyFullPath -AclObject $acl +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-AlkamiConfiguration.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-AlkamiConfiguration.ps1 new file mode 100644 index 0000000..cc6c6a8 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-AlkamiConfiguration.ps1 @@ -0,0 +1,64 @@ +function Set-AlkamiConfiguration { +<# +.SYNOPSIS + Used to configure Alkami specific settings to help ensure a smooth developer experience + +.PARAMETER Configuration + This is a shortened variable for the configuration being set to the chosen value + Aliased to: Setting, Configuration, Key + +.PARAMETER On + Aliased to: Enable + +.PARAMETER Off + Aliased to: Disable + +.PARAMETER Default + Aliased to: Remove +#> + [CmdletBinding(DefaultParameterSetName = 'On')] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet('InstallerUseSymlink','StartServicesOnInstall')] + [Alias('Setting')] + [Alias('Key')] + [string]$Configuration, + [Parameter(ParameterSetName = 'On')] + [Alias('Enable')] + [switch]$On, + [Parameter(ParameterSetName = 'Off')] + [Alias('Disable')] + [switch]$Off, + [Parameter(ParameterSetName = 'Default')] + [Alias('Remove')] + [switch]$Default + ) + + switch ($Configuration) { + 'StartServicesOnInstall' { + $variableName = 'Alkami.Installer.StartOnInstall' + $isEnvironmentVariable = $true + } + 'InstallerUseSymlink' { + $variableName = 'Alkami.Installer.UseSymlink' + $isEnvironmentVariable = $true + } + } + + if ($isEnvironmentVariable) { + if (!$Off -and !$Default) { + Set-EnvironmentVariable -Name $variableName -Value 'true' -StoreName User + Set-EnvironmentVariable -Name $variableName -Value 'true' -StoreName Process + } + + if ($Off) { + Set-EnvironmentVariable -Name $variableName -Value 'false' -StoreName User + Set-EnvironmentVariable -Name $variableName -Value 'false' -StoreName Process + } + + if ($Default) { + Remove-EnvironmentVariable -Name $variableName -StoreName User + Remove-EnvironmentVariable -Name $variableName -StoreName Process + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-HostFileTarget.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-HostFileTarget.ps1 new file mode 100644 index 0000000..de2f357 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-HostFileTarget.ps1 @@ -0,0 +1,56 @@ +function Set-HostFileTarget { +<# + +.SYNOPSIS + Allows a user to quickly switch their host file to target either localhost or the remote for dev environment URLs + +.DESCRIPTION + This function lets users swap between either having their host file point at the local environment (aka localhost) + or not when resolving dev environment URLs. This allows developers to do local development of services and quickly + switch to testing on the dev environments. This is accomplished by commenting and uncommenting any of the lines in + the host file that point at 127.0.0.1 and have a url in the form of "*.dev.alkamitech.com" + +.PARAMETER Remote + Switch to say to point to the remote when accessing these URLs. Either this switch or the -Local switch must be + specified, but not both + +.PARAMETER Local + Switch to say to point to the localhost when accessing these URLs. Either this switch or the -Local switch must be + specified, but not both + +.EXAMPLE + Set-HostFileTarget -Local + +.EXAMPLE + Set-HostFileTarget -Remote + +#> + [CmdletBinding()] + [OutputType([void])] + param ( + # CmdletBinding gives you these neat attributes to use + [Parameter(ParameterSetName = 'Remote')] + [switch]$Remote, + [Parameter(ParameterSetName = 'Local')] + [switch]$Local + ) + + $HostFilePath = "$env:windir\System32\drivers\etc\hosts" + $HostFile = Get-Content $HostFilePath + + if ($Local) { + $HostFile | Foreach { + # Things that are commented out should become uncommented + if ($_.StartsWith('#127.0.0.1') -and $_.EndsWith('dev.alkamitech.com') -and -not $_.Contains('orion.dev.alkamitech.com') -and -not $_.Contains('local.dev.alkamitech.com')) { + $_.Replace('#', '') + } else {$_} + } | Out-File $HostFilePath -enc ascii + } else { + $HostFile | Foreach { + # Things that are not commented out should become commented + if ($_.StartsWith('127.0.0.1') -and $_.EndsWith('dev.alkamitech.com') -and -not $_.Contains('orion.dev.alkamitech.com') -and -not $_.Contains('local.dev.alkamitech.com')){ + "#" + $_ + } else {$_} + } | Out-File $HostFilePath -enc ascii + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKAppPoolUsers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKAppPoolUsers.ps1 new file mode 100644 index 0000000..08246e0 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKAppPoolUsers.ps1 @@ -0,0 +1,72 @@ +function Set-SDKAppPoolUsers { +<# +.SYNOPSIS + Set the ApplicationPool users to the app pool in question +#> + [CmdletBinding(DefaultParameterSetName = 'Specified')] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'SDKUserMatrixEntry')] + [object]$SDKUserMatrixEntry, + [Parameter(Mandatory = $true, ParameterSetName = 'Specified', Position = 0)] + [Alias('Username')] + [string]$AppPoolName, + [Parameter(Mandatory = $true, ParameterSetName = 'Specified', Position = 1)] + [string]$Identity, + [Parameter(Mandatory = $false, ParameterSetName = 'Specified', Position = 2)] + [securestring]$Password + ) + begin { + $logLead = Get-LogLeadName + + Import-Module WebAdministration + } + process { + if ($PSCmdlet.ParameterSetName -eq 'SDKUserMatrixEntry') { + $Identity = $SDKUserMatrixEntry.DomainUsername + $AppPoolName = $SDKUserMatrixEntry.AppPoolName + $Password = $null + } + + if ([string]::IsNullOrWhiteSpace($AppPoolName)) { + # Even tho this is required, it could be null from the other parameter + Write-Verbose "$logLead : Empty AppPoolName for Identity [$Identity]. Nothing to do." + return + } + + # [Microsoft.Web.Administration.ProcessModelIdentityType]::ApplicationPoolIdentity is an internal class + # group Managed Service Accounts are considered SpecificUser + # $LocalSystem = 0 + # $LocalService = 1 + # $NetworkService = 2 + $SpecificUser = 3 + $ApplicationPoolIdentity = 4 + + # Start with the simplest thing possible here, then step up from there + $processModelValue = @{ + identitytype = $ApplicationPoolIdentity + } + + if ($null -ne $Password) { + Write-Debug "$logLead : Updating app pool with password: " $AppPoolName + $processModelValue = @{ + userName = $Identity + password = $Password + identitytype = $SpecificUser + } + } else { + if ($Identity -ne 'ApplicationPoolIdentity') { + Write-Host "$logLead : Updating [$AppPoolName] app pool with identity [$Identity]" + $processModelValue = @{ + userName = $Identity + identitytype = $SpecificUser + } + } + else { + Write-Host "$logLead : Updating [$AppPoolName] app pool with built-in identity."; + } + } + + Set-ItemProperty IIS:\AppPools\$AppPoolName -name processModel -value $processModelValue + # Start-WebAppPool -Name $AppPoolName + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKCertificateUsers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKCertificateUsers.ps1 new file mode 100644 index 0000000..b042296 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKCertificateUsers.ps1 @@ -0,0 +1,22 @@ +function Set-SDKCertificateUsers { + [CmdletBinding()] + param( + [string[]] $permittedIdenties + ) + + # TODO: See if we can fetch these from the server or splat them from another function and not list them here. + $rpsts2018 = '8edb140f1c84d4edcff730ea317662607218e5d9'; + $service2018 = '21014a433bb309665d1a14c3278cc7bd4d8c1c93'; + $client2018 = '692ddd519457eb6d943709b3ab0eb7ecc1945453'; + $token2018 = '9d6c7985c0c94eae6d3fda358e044a715bba50b8'; + + foreach ($thumbprint in @($token2018,$service2018,$client2018,$rpsts2018)) { + Write-Host "Updating thumbprint [$thumbprint] for the following identities..." + foreach($identity in $permittedIdenties) { + Write-Host $identity + Set-AclOnCert -Thumbprint $thumbprint -Identity $identity -FileSystemRights "FullControl" -Type "Allow" -StoreName "My" + Set-AclOnCert -Thumbprint $thumbprint -Identity $identity -FileSystemRights "FullControl" -Type "Allow" -StoreName "TrustedPeople" + } + } +} + diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKDatabaseUsers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKDatabaseUsers.ps1 new file mode 100644 index 0000000..db1b231 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKDatabaseUsers.ps1 @@ -0,0 +1,48 @@ +function Set-SDKDatabaseUsers { + [CmdletBinding()] + param( + [string] $source, + [string] $alkamiMasterDatabaseName, + [string] $alkamiDeveloperDatabaseName + ) + + # TODO: Get the locally configured master database connection string, filter all tenants to localhost/127.0.0.1/current machine name, then swing all of them + master + alkamimaster + + $connectionString = "data source=$($source);Integrated Security=SSPI; Database=dbname" + if ($source -match 'localhost') { + + # master (system) + $masterString = ($connectionString -replace 'dbname', 'master') + if (Test-DatabaseExists $masterString 'master') { + Write-Host "Running master (system) tasks..." + Write-Host "Connection String: " $masterString + + Remove-LegacyDatabaseUsers $masterString 'master' + Add-LocalServiceAccountsToDatabaseServer $masterString + Write-Host "Done." + } + + # alkamimaster + $alkamiMasterString = ($connectionString -replace 'dbname', $alkamiMasterDatabaseName) + if (Test-DatabaseExists $alkamiMasterString $alkamiMasterDatabaseName) { + Write-Host "Running master and alkamimaster tasks..." + Write-Host "Connection String: " $alkamiMasterString + + Remove-LegacyDatabaseUsers $alkamiMasterString $alkamiMasterDatabaseName + Add-LocalServiceAccountsToAlkamiDatabase $alkamiMasterString $alkamiMasterDatabaseName + + Write-Host "Done." + } + + # developer dynamic + $devDynamicString = ($connectionString -replace 'dbname', $alkamiDeveloperDatabaseName) + if (Test-DatabaseExists $devDynamicString $alkamiDeveloperDatabaseName) { + Write-Host "Running developer dynamic tasks..." + Write-Host "Connection String: " $devDynamicString + + Remove-LegacyDatabaseUsers $devDynamicString $alkamiDeveloperDatabaseName + Add-LocalServiceAccountsToAlkamiDatabase $devDynamicString $alkamiDeveloperDatabaseName + Write-Host "Done." + } + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServicePermissions.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServicePermissions.ps1 new file mode 100644 index 0000000..241a8e4 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServicePermissions.ps1 @@ -0,0 +1,109 @@ +Function Set-SDKServicePermissions { + param( + [string]$dbmsUser, + [string]$microUser, + [string]$databaseName, + [string]$databaseSource + ) + + $logLead = Get-LogLeadName + + ## We'll use the database permitter to grant access to any dbms services + $pathToPermitter = "C:\ProgramData\chocolatey\lib\Alkami.MicroServices.Choco.Installer.Database\tools\Alkami.Database.Permitter.exe" + + # We just want to make sure everything is off before we try to do this + Stop-SDKServices + + $chocoRootPath = Get-ChocolateyInstallPath + $chocoLibPath = Join-Path -Path $chocoRootPath -ChildPath 'lib' + + $serviceNames = (Get-AlkamiServices).Name + foreach ($serviceName in $serviceNames) { + + Write-Debug "Baking $serviceName..."; + + $chocoPackagePath = Join-Path -Path $chocoLibPath -ChildPath $serviceName + if (-not (Test-Path -Path $chocoPackagePath)) { + Write-Warning "$logLead : Could not find the chocolatey package at [$chocoPackagePath], continuing to next package" + continue + } + + ## Own the service, this allows us to make changes to it + Write-Debug "Owning service..."; + Invoke-SCExe @('config',$serviceName,'type=','own') + + $manifest = $null + try { + $manifest = Get-PackageManifest -Path $chocoPackagePath + } catch {} + if ($null -ne $manifest) { + # found a manifest + if ($null -ne $manifest.ServiceManifest) { + # found a service manifest + + # Assume that the service does not need to use the dbms user + $message = "Serice does not require access to the database" + $accountName = $microUser + if (Test-ServiceManifestRequiresDbAccess -ServiceManifest $manifest.ServiceManifest) { + # Service needs to use the dbms user + $message = "Service requires access to the database" + $accountName = $dbmsUser + } + + Write-Host "$logLead : $message. Configuring to use [$accountName] for [$serviceName]" + Invoke-SCExe @('config', $serviceName, 'obj=', $accountName) + + # Skipping the legacy applier because that should have been done on a successful install. Another function should reapply migrations as required + } else { + Write-Warning "$logLead : Manifest found is not a service manifest at [$chocoPackagePath]" + } + + continue + } # else fallback to the legacy path + + ## Service relative pathing for the various tasks we'll be doing + $toolsPath = "c:\programdata\chocolatey\lib\$serviceName\tools"; + $configPath = "c:\programdata\chocolatey\lib\$serviceName\tools\$serviceName.exe.config"; + $dbConfigPath = (Join-Path -Path $toolsPath -ChildPath "DatabaseConfig.ps1"); + + if(Test-Path -Path $configPath) { + + # TODO: This is now handled by the migration runner + ## test if dbms service + if(Test-Path -Path $dbConfigPath) { + Write-Debug "DatabaseConfig.ps1 detected: $dbConfigPath"; + + ## Each service has a DatabaseConfig.ps1 that defines the database role for the service and the migrations library + ## We'll use the $schemaGroupRole defined here as the role to add to the database + . $dbConfigPath + + ## Change the service to run as the dbms user + Write-Debug "Setting service user name...$dbmsUser" + Invoke-SCExe @('config',$serviceName,'obj=',$dbmsUser) + + ## Run the permitter in the migrations folder to create and assign roles to the already existing dbms user + Write-Debug "Permitting user for db roles..." + $connectionString = "data source=$databaseSource;Integrated Security=SSPI; Database=$databaseName"; + & $pathToPermitter $connectionString $dbmsUser $schemaGroupRole; + } + else { + Write-Debug "Logical service detected: $configPath"; + + ## Update the microservices to run as our micro user. + Write-Debug "Setting service user name...$microUser" + Invoke-SCExe @('config',$serviceName,'obj=',$microUser) + } + } + else { + Write-Debug "Unable to verify configuration file for $serviceName. Skipping." + } + } + + # TODO: Verify this user is a valid user to be set + # And Radium... + $radiumuser = "CORP\dev.radium$" + $radiumServiceName = "Alkami Radium Scheduler Service"; + Invoke-SCExe @('config',$radiumServiceName,'obj=',$radiumuser) + + Write-Debug "Done baking."; +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceRecovery.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceRecovery.ps1 new file mode 100644 index 0000000..1ed4604 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceRecovery.ps1 @@ -0,0 +1,52 @@ +Function Set-SDKServiceRecovery { +<# +.SYNOPSIS +Set the recovery options to restart for all Alkami SDK services. + +.DESCRIPTION +Change the recovery options to "Restart the Service" on failure for all Windows services associated with the SDK. + +.EXAMPLE +Set-SDKServiceRecovery -ServiceName 'All' -Action 'TakeNoAction' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host' -Action 'TakeNoAction' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host','Alkami.MS.TransactionEnrichment.Service.Host' -Action 'TakeNoAction' + +Omitting the Action parameter will use the default action of "Take No Action" +Set-SDKServiceRecovery -ServiceName 'All' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host','Alkami.MS.TransactionEnrichment.Service.Host' + +Set-SDKServiceRecovery -ServiceName 'All' -Action 'recovery' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host' -Action 'recovery' +Set-SDKServiceRecovery -ServiceName 'Alkami.MicroServices.Transactions.Service.Host','Alkami.MS.TransactionEnrichment.Service.Host' -Action 'recovery' + +.NOTES +General notes +#> + [CmdletBinding()] + param + ( + [Parameter(Mandatory=$true)] + [string[]]$ServiceName, + [Parameter(Mandatory=$false)] + [string]$Action + ) + + $actions = "//////"; + + #Write-Host "Action: $($Action)"; + + if (($PSBoundParameters.ContainsKey('Action')) -and ($Action -ne 'TakeNoAction')) { + $actions = "restart/60000//////" + } + + if (($ServiceName -contains 'All') -or ($ServiceName -contains 'all')) { + $ServiceName = (Get-AlkamiServices).Where({$_.Name -ne 'Alkami.Sidekick.Client'}).Name + } + + foreach ($service in $ServiceName) { + #Write-Host "Setting recovery on $($service) service to 'TakeNoAction'"; + # sc.exe failure $service reset= 86400 actions= $actions | Out-Null + Invoke-SCExe @("failure",$service,"actions=",$actions,"reset=",86400) + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceStartupType.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceStartupType.ps1 new file mode 100644 index 0000000..54a4839 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKServiceStartupType.ps1 @@ -0,0 +1,35 @@ +Function Set-SDKServiceStartupType { +<# +.SYNOPSIS +Set the start mode to manual for all Alkami SDK services. + +.DESCRIPTION +Change the start mode to "Manual" for all Windows services associated with the SDK. + +.EXAMPLE +Set-SDKServiceStartupType -ServiceName 'All' -StartupType 'Automatic' +Set-SDKServiceStartupType -ServiceName 'All' -StartupType 'Manual' +Set-SDKServiceStartupType -ServiceName @('Alkami.MicroServices.Transactions.Service.Host','Alkami.MS.TransactionEnrichment.Service.Host') -StartupType 'Manual' + +.NOTES +General notes +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string[]]$ServiceName, + [Parameter(Mandatory=$false)] + [ValidateSet('Automatic','AutomaticDelayedStart','Disabled','InvalidValue','Manual')] + [string]$StartupType + ) + + if (($ServiceName -contains 'All') -or ($ServiceName -contains 'all')) { + Write-Host "Setting startup mode on all services to $($mode)"; + $serviceName = (Get-ServiceInfoByCIMFragment C:\programdata\chocolatey\lib\).Where({$_.Name -match 'Alkami' -and $_.Name -ne "Alkami.Sidekick.Client"-and ($_.Name -ne "Alkami Radium Scheduler Service")}).Name + } + + foreach ($service in $ServiceName) { + Write-Host "Setting startup mode on $service to $StartupType"; + Set-Service -Name $service -StartupType $StartupType + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKTenant.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKTenant.ps1 new file mode 100644 index 0000000..ec11474 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKTenant.ps1 @@ -0,0 +1,51 @@ +function Set-SDKTenant { +<# +.SYNOPSIS + Add a list of tenants to your local environment. + +.DESCRIPTION + Create a new tenant instances on your machine by connecting to a remote tenant database and configuring their websites on your local machine. + +.PARAMETER Tenant + An array of tenant identifiers to bring into your environment. + +.EXAMPLE + Get-SDKTenant -ServerName "remote_db" -MasterDatabaseName "AlkamiMaster_Dev1" | Where-Object { $_.Name -eq "Altura" } | ForEach-Object { Set-SDKTenant $_ } + +.NOTES + General notes +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [object]$Tenant + ) + + $cert = (Get-ChildItem cert:\LocalMachine\My | Where-Object { $_.Subject -like "*``*.dev.alkamitech.com*" } -ErrorAction Ignore); + if ($null -eq $cert) { + throw "could not find the dev.alkamitech.com cert! Please install the Alkami developer certificates!" + } + + Import-TenantsToServer -ConnectionString (Get-MasterConnectionString) -Tenants $Tenant + + # Set all the client sigs + $tenantSignatures = $Tenant.Signature -split ',' + foreach ($signature in $tenantSignatures) { + Update-HostsFileEntry -IpAddress '127.0.0.1' -Hostname $signature + if (!(Get-WebBinding -Name "WebClient" -Protocol https -Port 443 -IPAddress * -HostHeader $signature)) { + WebAdministration\New-WebBinding -Name "WebClient" -Protocol https -Port 443 -IPAddress * -HostHeader $signature -SslFlags 1 | Out-Null + (Get-WebBinding -Name "WebClient" -Protocol https -Port 443 -IPAddress * -HostHeader $signature).AddSslCertificate($cert.Thumbprint, "my"); + } + } + + # Set all the admin sigs + $tenantAdminSignatures = $Tenant.AdminSignature -split ',' + foreach ($signature in $tenantAdminSignatures) { + Update-HostsFileEntry -IpAddress '127.0.0.1' -Hostname $signature + if (!(Get-WebBinding -Name "WebClientAdmin" -Protocol https -Port 443 -IPAddress * -HostHeader $signature)) { + WebAdministration\New-WebBinding -Name "WebClientAdmin" -Protocol https -Port 443 -IPAddress * -HostHeader $signature -SslFlags 1 | Out-Null + (Get-WebBinding -Name "WebClientAdmin" -Protocol https -Port 443 -IPAddress * -HostHeader $signature).AddSslCertificate($cert.Thumbprint, "my"); + } + } +} diff --git a/Modules/Alkami.PowerShell.SDK/Public/Set-SDKUsers.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKUsers.ps1 new file mode 100644 index 0000000..3f4b098 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Set-SDKUsers.ps1 @@ -0,0 +1,86 @@ +Function Set-SDKUsers { +<# +.SYNOPSIS +Set the default SDK users for the current local development environment + +.DESCRIPTION +For more information, see https://confluence.alkami.com/display/ISG/Alkami+SDK+Developer+Guide + +.EXAMPLE +choco upgrade alkami.machinesetup.sdk -y +Set-SDKUsers + +.PARAMETER DatabaseUser +Each service that connects to a database will be ran as this user name, will default to Alkami gMSA dev defaults. + +.PARAMETER NonDatabaseUser +Services that do not connect to a database will be ran as this user name, will default to Alkami gMSA dev defaults + +.PARAMETER EnvironmentType +The machine.config value for this key will be updated to match the value of the argument, will default to "Development". + +.PARAMETER EnvironmentName +The machine.config value for this key will be updated to match the value of the argument, will default to "Development". + +.PARAMETER DevelopmentDatabase +The developers target database, will default to "DeveloperDynamic" + +.PARAMETER TenantDatabase +The developers target tenant database, will default to "AlkamiMaster" + +.PARAMETER CertificateUsers +The users we should grant certificate permissions for, will default to @('dev.nag$', 'dev.radium$', 'dev.micro$', 'dev.dbms$') + +.NOTES +This version uses the Alkami dev.* gMSA defaults. +#> + + [CmdletBinding()] + param( + [string]$DatabaseUser = "CORP\dev.dbms$", + [string]$NonDatabaseUser = "CORP\dev.micro$", + [string]$EnvironmentType = 'Development', + [string]$EnvironmentName = 'Development', + [string]$DevelopmentDatabase = 'DeveloperDynamic', + [string]$TenantDatabase = 'AlkamiMaster', + [string[]] $CertificateUsers = @('dev.nag$', 'dev.radium$', 'dev.micro$', 'dev.dbms$') + ) + + Write-Debug "Ensuring environment users have been added to the local security group" + Add-UsersToLocalSecurityGroup @($DatabaseUser, $NonDatabaseUser) "Performance Monitor Users" + + Write-Host "Updating environment, this may take some time." + Set-AppSetting 'Environment.Type' $EnvironmentType + Set-AppSetting 'Environment.Name' $EnvironmentName + + Set-AppSetting 'NonDatabaseMicroServiceAccount' $NonDatabaseUser + Set-AppSetting 'DatabaseMicroServiceAccount' $DatabaseUser + + Write-Debug "Updating SMSvcHostSids" + Set-SMSvcHostSids + Remove-SMSvcHostBlankSecurityIdentifiers + + Write-Debug "Stopping Alkami Services" + Stop-SDKServices + + Write-Debug "Restarting Windows Process Activation Service" + Restart-Service @("WAS") -Force + + Write-Debug "Updating Database Users" + Set-SDKDatabaseUsers 'localhost' $TenantDatabase $DevelopmentDatabase + + Write-Debug "Updating IIS Application Pools" + Invoke-CallOperatorWithPathAndParameters -Path "C:\WINDOWS\system32\iisreset.exe" -Arguments @("/stop") + Get-SDKUserMatrix | Set-SDKAppPoolUsers + Invoke-CallOperatorWithPathAndParameters -Path "C:\WINDOWS\system32\iisreset.exe" -Arguments @("/start") + + Write-Debug "Updating Installed Microservices" + Set-SDKServicePermissions $DatabaseUser $NonDatabaseUser $DevelopmentDatabase 'localhost' + + Write-Debug "Updating Alkami Common Certificates" + Set-SDKCertificateUsers $CertificateUsers + + # Write-Debug "Installing Alkami Vendor Certificates Chocolatey Package" + # & choco upgrade Alkami.DeveloperKit.Certificates.Employee.Vendors -y + Write-Host "Environment users updated. You may Start-SDKServices when you are ready to continue, or now may be a good time to reboot." +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Start-SDKServices.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Start-SDKServices.ps1 new file mode 100644 index 0000000..846168e --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Start-SDKServices.ps1 @@ -0,0 +1,48 @@ +function Start-SDKServices { + + <# + .SYNOPSIS + Starts both IIS and Alkami Windows Services + + .DESCRIPTION + Starts both IIS and Alkami Windows Services. Max service start parallelism defaults to 10 and can be set by specifying a positive int for the parameter maxParallel. + IIS is started first. Dependent windows services are started next serially, via Start-DependentServices. Remaining Alkami services are started last + + .PARAMETER maxParallel + [int] The maximum number of services to start in parallel. Defaults to 10 + + .EXAMPLE + Start-IISAndServices -maxParallel 2 + +[Start-IISAndServices] : Service start parallelism set to 2 +[Start-IISOnly] : Starting IIS... +[Start-IISOnly] : Done +[Start-IISAndServices] : Sleeping for 5 seconds... +[Start-DependentServices] : No Dependent Services Found to Start +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 3 chocolatey services. +[Start-IISAndServices] : No Services Found to Start + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$maxParallel = 10 + ) + + $logLead = Get-LogLeadName + if ($maxParallel -lt 1) { + Write-Host "$logLead : Service start parallelism set to $maxParallel" + } + + Write-Host "Restarting redis-master18620 and redis-slave18621" + restart-service @("redis-master18620", "redis-slave18621") -Force + + Start-IISOnly + + Write-Host "$logLead : Sleeping for 5 seconds..." + Start-Sleep -Seconds 5 + + Start-ServicesOnly -maxParallel $maxParallel +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Stop-SDKServices.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Stop-SDKServices.ps1 new file mode 100644 index 0000000..77e6dc2 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Stop-SDKServices.ps1 @@ -0,0 +1,23 @@ +Function Stop-SDKServices { +<# +.SYNOPSIS +Stop all the running Alkami SDK services. + +.DESCRIPTION +Stop all Alkami Windows services associated with the SDK, including any hung processes. + +.EXAMPLE +Stop-SDKServices + +.NOTES +General notes +#> + [CmdletBinding()] + param( + ) + + $serviceNames = (Get-ServiceInfoByCIMFragment C:\programdata\chocolatey\lib\).Where({$_.Name -match 'Alkami' -and $_.Name -ne "Alkami.Sidekick.Client" -and $_.ProcessId -gt 0}).Name + foreach ($serviceName in $serviceNames) { + Stop-AlkamiService -ServiceName $serviceName + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Uninstall-SDKIISComponents.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Uninstall-SDKIISComponents.ps1 new file mode 100644 index 0000000..945030f --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Uninstall-SDKIISComponents.ps1 @@ -0,0 +1,113 @@ +function Uninstall-SDKIISComponents { + Import-Module WebAdministration + + $appPoolNames = @("WebClientAdmin","AuditService","BankService","WebClient","Client","Admin","ContentService","CoreService","ExceptionService","IPSTS","MessageCenterService","NagConfigurationService","NotificationService","RP-STS","SchedulerService","SecurityManagementService","STSConfiguration","symconnectmultiplexer","root") + + Function GetTemporaryASPNetDirectory { + return "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\" + } + + Function ClearIISTempFiles($appPools) { + $appPools = ($appPools, @() -ne $null)[0] + + $appPools | % { + $appPool = $_ + try { + $path = (Join-Path (GetTemporaryASPNetDirectory) $appPool) + if (Test-Path $path) { + [AlkamiDirectories]::FlushDirectory($path) + } + } catch { + Write-Warning "could not delete temp files for app pool $appPool" + } + } + } + + $copyDir = @" + using System; + using System.IO; + using System.Collections.Generic; + using System.Linq; + + public class AlkamiDirectories + { + public static void FlushDirectory(string directoryName) { + if (Directory.Exists(directoryName)) { + try { + Directory.Delete(directoryName, true); + } + catch (Exception ex) + { + System.Console.WriteLine(string.Format("could not delete {0}. ex.message {1}", directoryName, ex.Message)); + Directory.Delete(directoryName, true); + } + } + } + } +"@; + + Add-Type -TypeDefinition $CopyDir + + $installPath = split-path -parent $MyInvocation.MyCommand.Path + + ############################################################################### + ## install/configure ORB + ## TODO: Copy this into the right path and then include it. + + iisreset /stop /timeout:60 + + Uninstall-AlkamiService -Path (Join-Path (Get-OrbPath) 'Radium') + + @( + "AuditService" + "BankService" + "ContentService" + "CoreService" + "ExceptionService" + "MessageCenterService" + "NagConfigurationService" + "NotificationService" + "RP-STS" + "STSConfiguration" + "SchedulerService" + "SecurityManagementService" + "SymConnectMultiplexer" + "CUFX" + "OrbFX" + "TextBanking" + ) | % { + if (Get-WebApplication -Name $_ -Site "Client") { + Remove-WebApplication -Name $_ -Site "Client" + } + ## Ensure that the app exists (!!(Get ... and then ensure that there are NO applications attached to it (!xyz.Count == true if xyz.Count = 0) + if (!!(Get-IisAppPool -Name $_) -and !(@(Get-WebConfigurationProperty "/system.applicationHost/sites/site/application[@applicationPool=`'$_`']" "machine/webroot/apphost" -name path).Count)) { + Remove-WebAppPool -Name $_ + } + } + + @( + "WebClient" + "IPSTS" + "WebClientAdmin", + "Client", + "Admin", + "developer.dev.alkamitech.com", + "IP-STS" + ) | % { + if (Test-Path IIS:\Sites\$_) { + Remove-WebSite -Name $_ + } + if (Test-Path IIS:\AppPools\$_) { + Remove-WebAppPool -Name $_ + } + } + + ClearIISTempFiles $appPoolNames + + $logDirectory = "C:\Orblogs" + if (!(Test-Path $logDirectory)) { + Remove-Item $logDirectory -Recurse -Force -ErrorAction Ignore + } + + iisreset /start +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Update-DeveloperModules.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Update-DeveloperModules.ps1 new file mode 100644 index 0000000..dea35f1 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Update-DeveloperModules.ps1 @@ -0,0 +1,62 @@ +function Update-DeveloperModules { +<# +.SYNOPSIS + Used to update developers modules in a consistent manner. + Useful for reducing manual module management by developers. + This function does not look for advanced modules to be downloaded, it only updates local modules. +#> + param ( + ) + + $logLead = (Get-LogLeadName) + + # Get all modules with SREModule AlkamiManifeset componentTypes + $lookInFolder = (Get-ChocolateyInstallPath) + + $allFilePaths = @() + $moduleNames = @() + + foreach ($filename in (Get-ValidPackageManifestFilenames)) { + $findPath = (Join-Path (Join-Path $lookInFolder 'lib') $filename) + $allFilePaths += @((Get-ChildItem $findPath -Recurse).FullName) + } + + foreach($filepath in $allFilePaths) { + if ([string]::IsNullOrWhiteSpace($filepath)) { + continue + } + + $packageManifest = (Get-PackageManifest -Path $filepath) + if (($packageManifest.general.componentType -eq 'SREModule') -or ($packageManifest.general.componentType -eq 'Installer')) { + $moduleNames += (Get-ChocoPackageFromPath -Path $filepath) + } + } + + if ($moduleNames.Count -gt 0) { + $arguments = @('upgrade','--no-progress','--confirm') + $arguments += $moduleNames + $chocoPath = (Join-Path (Join-Path $lookInFolder 'bin') 'choco.exe') + Write-Host "$logLead : $chocoPath @($arguments -join ' ')" + Invoke-CallOperatorWithPathAndParameters -Path $chocoPath -Arguments $arguments + Get-Module Alkami* | Remove-Module -Force + } else { + Write-Warning "$logLead : Could not find any packages under [$lookInFolder]. Have you upgraded to the latest modules before running this command?" + Write-Host "$logLead : Use this command string to update your modules to some variant of the latest, but this may be out of date`n`nchoco update Alkami.PowerShell.AD Alkami.PowerShell.Choco Alkami.PowerShell.Common Alkami.PowerShell.Configuration Alkami.PowerShell.Database Alkami.PowerShell.IIS Alkami.PowerShell.ServerManagement Alkami.PowerShell.ServiceFabric Alkami.PowerShell.Services Alkami.Installer.Provider Alkami.Installer.WebExtension Alkami.Installer.Widget Alkami.Installer.WebApplication Alkami.Installer.Services Alkami.Installer.WebSite -y`n`nIn the future this function should work for what you want." + } + + try { + if ((Get-ADDomain -ErrorAction Ignore).Forest -eq 'corp.alkamitech.com') { + if ((choco list carbon -lo) -match 'carbon' ) { + choco uninstall carbon -fy + Write-Warning " ==========" + Write-Warning " ATTENTION" + Write-Warning " ==========" + Write-Warning "Sorry folks, carbon is deterimental to the Alkami environment and we are asking all devs to remove it" + Write-Warning "It will now be uninstalled from your machine. If you have a need for it, please reach out to @cicd in #eng-crossfire" + Write-Warning "We want to help everyone, so if we can help with a task Carbon previously did, we want to make things better." + Write-Warning "" + Write-Warning "If you don't know why you had carbon installed, you can ignore this message" + } + } + } catch { <# purposefully ignore this #>} +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.SDK/Public/Update-HostsFileEntry.ps1 b/Modules/Alkami.PowerShell.SDK/Public/Update-HostsFileEntry.ps1 new file mode 100644 index 0000000..4e6e579 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/Public/Update-HostsFileEntry.ps1 @@ -0,0 +1,55 @@ +function Update-HostsFileEntry { +<# +.SYNOPSIS + Updates or adds a specific hosts file entry to the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [Parameter(Mandatory = $false)] + [string]$Comment + ) + + $logLead = Get-LogLeadName + + $records = @() + $existingRecords = Get-HostsFileAllRecords + + $foundPriorRecord = $false + foreach ($record in $existingRecords) { + if ($record.Hostname -eq $Hostname) { + $foundPriorRecord = $true + + $record.IpAddress = $IpAddress + # Only update the comment if we passed one in. Ignore the previous comment and overwrite (frequently this is for adding jira tickets on changes) + # We could alternately prepend this comment to the existing comment, with a comma+space separator + if (![string]::IsNullOrWhiteSpace($Comment)) { + $record.Comment = $Comment + } + + $formattedRecord = Format-HostsFileRecord -Record $record + + Write-Host "$logLead : Updating record for $formattedRecord" + } + $records += $record + } + + if (!$foundPriorRecord) { + $newRecord = New-HostsFileEntry -IpAddress $IpAddress -Hostname $Hostname -Comment $Comment + $formattedRecord = Format-HostsFileRecord -Record $record + + Write-Host "$logLead : Adding record for $formattedRecord" + + $records += $newRecord + } + + Save-CompleteHostsFile -Record $records +} diff --git a/Modules/Alkami.PowerShell.SDK/pointtoqa_sdk.ps1 b/Modules/Alkami.PowerShell.SDK/pointtoqa_sdk.ps1 new file mode 100644 index 0000000..f9d3aa0 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/pointtoqa_sdk.ps1 @@ -0,0 +1,673 @@ +Param( + [Parameter(Mandatory=$false,Position=1)][string]$intendedName = "QA2" +) +process { + $nameMatrix = @{ + 'dev' = @(); + 'qa2' = @(); + 'red9' = @(); + }; + + $Certs = @{ + 'dev' = @{ Name = 'DEV'; Database = 'AlkamiMaster_Dev1'; HostnameBase = 'dev.alkamitech.com'; Broadcaster = ''; Subscription = 'localhost'; }; + 'qa2' = @{ Name = 'QA2'; Database = 'AlkamiMaster_QA2'; HostnameBase = 'QA2.alkamitech.com'; Broadcaster = '10.0.12.59'; Subscription = '10.0.12.59'; }; + 'red9' = @{ Name = 'RED9'; Database = 'AlkamiMaster_Red9'; HostnameBase = 'red9.dev.alkamitech.com'; Broadcaster = '10.27.0.201'; Subscription = '10.27.0.201'; }; + }; + + + $name = ""; + if ($nameMatrix.ContainsKey($intendedName.ToLower())){ + $name = $intendedName; + } else { + $name = @($nameMatrix.Keys | % { if ($nameMatrix.Item($_) -icontains $intendedName.ToLower()) { return $_; } })[0]; + } + + if ([String]::IsNullOrEmpty($name)) { + throw "could not find the name $name"; + } + +<# +[3:38 PM] Cole Brand: db reset-tenants -masterDatabaseName AlkamiMaster_QA7; install -rebind +[4:14 PM] Lance Turri: In IIS, update all bindings to the QA server and update cert to the respective server. +In your HOSTS file, update all entries from *.dev.* to *.[QASERVER].* +Ensure your machine.config points to the subscription service and broadcaster ip address of that QA server. +Clear out ASP.NET temporary cache files. +[4:15 PM] Lance Turri: It seems like the reset tenants call will update the bindings and replace the entires in the host file, but what about the machine config? +[4:25 PM] Cole Brand: Oh, loading the cert +[4:25 PM] Cole Brand: we need some way to get the cert +[4:25 PM] Cole Brand: the machine config was the part I explicitly need to write into a single file + + ++ db sync tenants from given db master ++ hosts sync tenants from tenant +add site for new server addresses (*.qa9.alkamitech.com, etc) ++ update machine.config subscription server name +#> + +Function EnsureDatabaseAccess($connectionString) { + try + { + if ($script:v) { + Write-Host "Attempting to verify the connection to $connectionString"; + } + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection "$connectionString"; + $sqlConnection.Open(); + $sqlConnection.Close(); + + return; + } + catch + { + Write-Host "Can not connect to the specified database. Do you have approved access to the server?"; + throw "could not connect to the database!" + } +} + +Function GetDefaultLocalConnectionString() { + $x = [Xml](Get-Content "C:\Windows\Microsoft.Net\Framework64\v4.0.30319\Config\machine.config"); + try { + return $x.configuration.connectionStrings.add | % { if ($_.Name -eq "AlkamiMaster") { $_.ConnectionString } }; + } catch { + return $null; + } +} + +Function GetFormattedConnectionString($serverName, $instanceName, $databaseName) { + if ([System.String]::IsNullOrEmpty($serverName) -and [System.String]::IsNullOrEmpty($instanceName) -and [System.String]::IsNullOrEmpty($databaseName)) { + return (GetDefaultLocalConnectionString); + } + + $serverName = ($serverName, "." -ne $null)[0]; + + if ($serverName.EndsWith("\\")){ + $serverName = $serverName.Substring(0, $serverName.Length - 1); + } + + $connectionString = "data source=$serverName\$instanceName;Integrated Security=SSPI; Database=$databaseName;"; + + if ($script:v) { + Write-Host $connectionString; + } + + return $connectionString; +} + +function GetFormattedConnectionStringForTenant($serverName, $databaseName) +{ + $serverName = ($serverName, "." -ne $null)[0]; + + if ($serverName.EndsWith("\\")){ + $serverName = $serverName.Substring(0, $serverName.Length - 1); + } + + $connectionString = "data source=$serverName;Integrated Security=SSPI; Database=$databaseName;"; + + if ($script:v) { + Write-Host $connectionString; + } + + return $connectionString; +} + +Function InsertTenantsToServer($serverName, $instanceName, $databaseName, $tenants) { + $tenants = ($tenants, @() -ne $null)[0]; + if ($tenants.length -eq 0) { + Write-Host "No tenants were provided. Can't update the database server $serverName\$instanceName $databaseName with the new records."; + } + $connectionString = (GetFormattedConnectionString $serverName $instanceName $databaseName); + EnsureDatabaseAccess $connectionString + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString; + + try + { + $sqlConnection.Open(); + + $tenants | % { + $tenant = $_; + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand(); + $command.CommandText = " + IF NOT EXISTS (SELECT * FROM Tenant WHERE BankIdentifiers = @BankGuid) BEGIN + INSERT INTO Tenant (Name,BankIdentifiers,BankUrlSignatures,CreateDate,BankAdminUrlSignatures,DataSource,Catalog,Version,ConnectionString) + VALUES (@Name,@BankGuid,@Signature,GETDATE(),@AdminSignature,@DataSource,@Catalog,@Version,@ConnectionString); + END + ELSE + BEGIN + UPDATE Tenant SET BankUrlSignatures = @Signature, BankAdminUrlSignatures = @AdminSignature, DataSource = @DataSource, Catalog = @Catalog, ConnectionString = @ConnectionString WHERE BankIdentifiers = @BankGuid + END + "; + + $consume = $command.Parameters.AddWithValue("@Name",$tenant.Name); + $consume = $command.Parameters.AddWithValue("@BankGuid",$tenant.BankGuid); + $consume = $command.Parameters.AddWithValue("@Signature",$tenant.Signature); + $consume = $command.Parameters.AddWithValue("@AdminSignature",$tenant.AdminSignature); + $consume = $command.Parameters.AddWithValue("@DataSource",$tenant.DataSource); + $consume = $command.Parameters.AddWithValue("@Catalog",$tenant.Catalog); + $consume = $command.Parameters.AddWithValue("@Version",$tenant.Version); + $consume = $command.Parameters.AddWithValue("@ConnectionString", $tenant.ConnectionString); + + $consume = $command.ExecuteNonQuery(); + } + + $sqlConnection.Close(); + } + catch + { + if ($null -ne $sqlConnection) { + try { + $sqlConnection.Close(); + } catch { + } + } + Write-Host "something happened bad..."; + $_; + throw $_.Exception; + } +} + +Function GetFullTenantsFromServer($serverName, $instanceName, $databaseName) { + $connectionString = (GetFormattedConnectionString $serverName $instanceName $databaseName); + EnsureDatabaseAccess $connectionString + + try + { + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString; + $sqlConnection.Open(); + + $query = " + SELECT + Name, + BankIdentifiers, + BankUrlSignatures, + BankAdminUrlSignatures, + DataSource, + Catalog, + Version + FROM Tenant;"; + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand(); + $command.CommandText = $query; + + [System.Data.SqlClient.SqlDataReader]$reader = $command.ExecuteReader(); + + $data = @(); + + while ($reader.Read()) { + $data += @{ + Name = $reader[0]; + BankGuid = $reader[1]; + Signature = $reader[2]; + AdminSignature = $reader[3]; + DataSource = $reader[4]; + Catalog = $reader[5]; + Version = $reader[6]; + ConnectionString = (GetFormattedConnectionStringForTenant "dc00db01" $reader[5]) + }; + } + + $sqlConnection.Close(); + + return $data; + } + catch + { + Write-Host "something happened bad..."; + $_; + return $null; + } +} + +Function UpdateTenants($databaseName) { + # always force it to localhost for this operation. + FlushTenantsFromServer "localhost" "" "AlkamiMaster" + $tenants = (GetFullTenantsFromServer (GetPrimaryDBServerForDev) "" $databaseName); + if($intendedName.ToLower() -eq "dev") + { + $tenants += GetDeveloperTenant; + } + InsertTenantsToServer "localhost" "" "AlkamiMaster" $tenants +} + +Function FlushTenantsFromServer($serverName, $instanceName, $databaseName) { + $connectionString = (GetFormattedConnectionString $serverName $instanceName $databaseName); + EnsureDatabaseAccess $connectionString + + $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $connectionString; + + try + { + $sqlConnection.Open(); + + $query = "DELETE FROM Tenant;"; + + [System.Data.SqlClient.SqlCommand]$command = $sqlConnection.CreateCommand(); + $command.CommandText = $query; + + $consume = $command.ExecuteNonQuery(); + + $sqlConnection.Close(); + + return $data; + } + catch + { + if ($null -ne $sqlConnection) { + try { + $sqlConnection.Close(); + } catch { + } + } + Write-Host "something happened bad..."; + $_; + throw $_.Exception; + } +} + +Function GetPrimaryDBServerForDev(){ + return "DC00DB01"; +} + +Function GetLocalDatabaseTenantsBankUrlSignatures() { + $tenants = (GetFullTenantsFromServer "localhost" "" "AlkamiMaster"); + $ret = $tenants.Signature | % { if ($_ -match ",") { return ($_ -split ",")[0] } else { return $_; } } + + return $ret; +} + +Function GetLocalDatabaseTenantsBankAdminUrlSignatures() { + $tenants = (GetFullTenantsFromServer "localhost" "" "AlkamiMaster"); + $tenants.AdminSignature + + return; +} + +Function GetKnownHostsEntries() { + @( + @{ ipAddress = "10.0.28.23"; hostname = "secure160.missionfed.com"; }, + @{ ipAddress = "10.0.28.93"; hostname = "bldsrvr-dev03.ftfcu.corp"; }, + @{ ipAddress = "10.0.28.50"; hostname = "rhpw065.efiserv.com"; }, + @{ ipAddress = "10.0.28.86"; hostname = "msc-imgtsweb01.iccu.com"; }, + @{ ipAddress = "10.0.28.72"; hostname = "dev-app.firsttechfed.com"; }, + @{ ipAddress = "10.0.28.81"; hostname = "qa1-app-farm.firsttechfed.com"; }, + @{ ipAddress = "10.0.28.116"; hostname = "qa2-app-farm.firsttechfed.com"; }, + @{ ipAddress = "10.0.29.16"; hostname = "apidev.veridiancu.org"; }, + @{ ipAddress = "10.0.29.19"; hostname = "olbvendor-dev.desertschools.net"; }, + @{ ipAddress = "10.0.29.19"; hostname = "olbvendor-qa.desertschools.net"; }, + @{ ipAddress = "192.168.119.219"; hostname = "onbase-acu.achievacu.com"; }, + @{ ipAddress = "192.168.119.65"; hostname = "acu-agw04.achievacu.com"; }, + @{ ipAddress = "216.189.225.47"; hostname = "uat.udi.local"; }, + @{ ipAddress = "10.26.73.113"; hostname = "SymConnectMultiplexer"; }, + @{ ipAddress = "127.0.0.1"; hostname = "AuditService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "BankService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "ContentService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "CoreService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "ExceptionService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "IP-STS"; }, + @{ ipAddress = "127.0.0.1"; hostname = "MessageCenterService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "NagConfigurationService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "NotificationService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "RP-STS"; }, + @{ ipAddress = "127.0.0.1"; hostname = "Scheduler"; }, + @{ ipAddress = "127.0.0.1"; hostname = "SecurityManagementService"; }, + @{ ipAddress = "127.0.0.1"; hostname = "STSConfiguration"; }, + @{ ipAddress = "127.0.0.1"; hostname = "redis-18620.redis.corp.alkamitech.com"; }, + @{ ipAddress = "127.0.0.1"; hostname = "ip.dev.alkamitech.com"; }, + + #todo: do we need these here still? can't they just be in the database? + @{ ipAddress = "127.0.0.1"; hostname = "developer.dev.alkamitech.com"; }, + @{ ipAddress = "127.0.0.1"; hostname = "admin-developer.dev.alkamitech.com"; }, + @{ ipAddress = "127.0.0.1"; hostname = "integration.dev.alkamitech.com"; }, + @{ ipAddress = "127.0.0.1"; hostname = "admin-integration.dev.alkamitech.com"; } + ) +} + +Function WriteHostsEntryToFile($hostsRenderedLines, $pathName) { + $hostsRenderedLines | Out-File $pathName -encoding ASCII +} + +Function SetOrbHostsComplete() { + WriteHostsEntryToFile (RenderCompleteHostsFile (GetHostsFileActualEntries (GetHostsFilePath)) (DeltaHostsFileAndExpectedEntries (GetHostsFilePath))) (GetHostsFilePath) +} + +Function GetHostsFileActualEntries($hostsFilePath) { + (Get-Content -Path $hostsFilePath) | % { + $rawline = $_; + $record = @{ Keep = $false; ipAddress = $null; hostname = $null; Comment = $null; blankLine = $false; }; + $commentSeparator = $rawline.IndexOf("#"); + $comment = ""; + $keep = $false; + + if ($commentSeparator -gt -1) { + $record.Comment = $rawline.Substring($commentSeparator + 1, $rawline.Length - $commentSeparator - 1).Trim(); + if ($commentSeparator -eq 0) { + $rawline = ""; + } else { + $rawline = $rawline.Substring(0, $commentSeparator - 1); + } + } + + if ($rawline.length -gt 0) { + $bits = [regex]::Split($rawline, "\s+") + if ($bits.count -gt 1) { + $record.ipAddress = $bits[0].Trim(); + $record.hostname = $bits[1].Trim(); + } + } + + $record.Keep = (($record.Comment -imatch 'keep') -or ($record.ipAddress -eq $null)); + $record.blankLine = (!$record.Comment -and !$record.ipAddress -and ($commentSeparator -eq -1)); + return $record; + } +} + +Function GetAllExpectedHostsEntries() { + $hostsEntries = @(); + $hostsEntries += (GetKnownHostsEntries); + $hostsEntries += @(((GetLocalDatabaseTenantsBankUrlSignatures) | % { @{ ipAddress = "127.0.0.1"; hostname = $_; } } )); + $hostsEntries += @(((GetLocalDatabaseTenantsBankAdminUrlSignatures) | % { @{ ipAddress = "127.0.0.1"; hostname = $_; } } )); + $hostsEntries; +} + +Function RenderCompleteHostsFile($entries, $newEntries) { + $entries = ($entries,@() -ne $null)[0]; + + $entries += @(($newEntries, @() -ne $null)[0]); + + $maxHostnameWidth = 0; + $entries | % { + $entry = $_; #readability line + if (!!$entry.hostname) { + if ($entry.hostname.length -gt $maxHostnameWidth) { + $maxHostnameWidth = $entry.hostname.length; + } + } + } + + if ($maxHostnameWidth -eq 0) { + return; + } else { + $maxHostnameWidth += 5; + } + + ($entries | % { + $entry = $_; #readability line + if (!$entry.ipAddress) { + if ($entry.blankLine) { + return ""; + } else { + return ("# {0}" -f $entry.Comment); + } + } else { + $thirdPart = ""; + $secondPart = $entry.hostname; + if (!!$entry.Comment) { + $thirdPart = "# $($entry.Comment)"; + $secondPart = $secondPart.PadRight($maxHostnameWidth); + } + return ("{0}{1}{2}" -f $entry.ipAddress.PadRight(18), $secondPart, $thirdpart); + } + }) | Get-Unique +} + +Function GetHostsFileActualEntries($hostsFilePath) { + (Get-Content -Path $hostsFilePath) | % { + $rawline = $_; + $record = @{ Keep = $false; ipAddress = $null; hostname = $null; Comment = $null; blankLine = $false; }; + $commentSeparator = $rawline.IndexOf("#"); + $comment = ""; + $keep = $false; + + if ($commentSeparator -gt -1) { + $record.Comment = $rawline.Substring($commentSeparator + 1, $rawline.Length - $commentSeparator - 1).Trim(); + if ($commentSeparator -eq 0) { + $rawline = ""; + } else { + $rawline = $rawline.Substring(0, $commentSeparator - 1); + } + } + + if ($rawline.length -gt 0) { + $bits = [regex]::Split($rawline, "\s+") + if ($bits.count -gt 1) { + $record.ipAddress = $bits[0].Trim(); + $record.hostname = $bits[1].Trim(); + } + } + + $record.Keep = (($record.Comment -imatch 'keep') -or ($record.ipAddress -eq $null)); + $record.blankLine = (!$record.Comment -and !$record.ipAddress -and ($commentSeparator -eq -1)); + return $record; + } +} + +Function GetHostsFilePath() { + return "$env:windir\System32\drivers\etc\hosts"; +} + +Function DeltaHostsFileAndExpectedEntries($hostsPath){ + $existingEntries = (GetHostsFileActualEntries $hostsPath); + $expectedEntries = (GetAllExpectedHostsEntries); + $missingEntries = @(); + + $expectedEntries | % { + $found = $false; + $expectedEntry = $_; + if (!!$expectedEntry.ipaddress) { + $existingEntries | % { + $existingEntry = $_; + if (!!($existingEntry.ipAddress) -and ($expectedEntry.hostname -eq $existingEntry.hostname) -and ($expectedEntry.ipAddress -eq $existingEntry.ipAddress)) { + $found = $true; + } + } + if (!$found) { + $missingEntries += $expectedEntry; + } + } + } + + $missingEntries; +} + +Function UpdateMachineConfigs($broadcaster, $subscription) { + UpdateMachineConfig 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config' $broadcaster $subscription; + UpdateMachineConfig 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config' $broadcaster $subscription; +} + +Function UpdateMachineConfig($path, $broadcaster, $subscription) { + $config = [Xml](get-content $path); + $config.configuration.appSettings.add | % { if ($_.Key -eq "SubscriptionServiceMachine") { $_.Value = $subscription; } }; + $config.configuration.appSettings.add | % { if ($_.Key -eq "Broadcasters") { $_.Value = $broadcaster; } }; + $config.Save($path); +} + +Function GetOrCreateAppPoolForOrb($appPoolName) { + $ret = Get-Item IIS:\AppPools\$appPoolName -ErrorAction Ignore; + if ($ret -eq $null) { + $ret = New-Item IIS:\AppPools\$appPoolName -ErrorAction Stop; + + Set-ItemProperty IIS:\AppPools\$appPoolName managedRuntimeVersion 'v4.0'; + Set-ItemProperty IIS:\AppPools\$appPoolName ProcessModel.loadUserProfile true; + Set-ItemProperty IIS:\AppPools\$appPoolName ProcessModel.idleTimeout -Value "0"; + } + return $ret; +} + +Function GetOrCreateWebsite($siteName, $physicalPath) { + Write-Host $siteName + Write-Host $physicalPath + $appPool = GetOrCreateAppPoolForOrb $siteName; + $ret = WebAdministration\Get-WebSite -Name $siteName + if ($ret -eq $null) { + $ret = WebAdministration\New-Website -Name $siteName -PhysicalPath $physicalPath -ApplicationPool $appPool.Name -ErrorAction Stop; + } + + if ($ret -eq $null){ + throw "could not create/find the website for $siteName! ARGH!"; + } + + return $ret; +} + +Function rebind-ipsts($site) { + Write-Host "rebing-ipsts" -ForegroundColor Yellow + $cert = (Get-ChildItem cert:\LocalMachine\My | Where { $_.Subject -like "*``*.$($site.HostnameBase)*" } -ErrorAction Ignore); + + $toBeInstalled = @("ip.$($site.HostnameBase)"); + + if (Test-Path 'C:\Orb\IP-STS') { + $consume = GetOrCreateWebsite 'IPSTS' 'C:\Orb\IP-STS' + } else { + $consume = GetOrCreateWebsite 'IPSTS' 'C:\Orb\IPSTS' + } + + #Remove-WebBinding -Name 'IPSTS' -IPAddress * -Port 80 -ErrorAction Ignore; + + if ($toBeInstalled -ne $null) { + Write-Host "Installing new client sites under ORB\Client"; + Write-Host -NoNewline "of $($toBeInstalled.Length) we are on "; + $counter = 0; + $toBeInstalled | %{ + $counter += 1; + $newSiteBinding = $_; + if (!(WebAdministration\Get-WebBinding -Name 'IPSTS' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding)) { + $consume = WebAdministration\New-WebBinding -Name 'IPSTS' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding -SslFlags 1 + (WebAdministration\Get-WebBinding -Name 'IPSTS' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding).AddSslCertificate($cert.Thumbprint, "my"); + } + Write-Host -NoNewline "$counter "; + } + Write-Host "done"; + } +} + +Function rebind-clients($rebindTier, $site) { + $toBeInstalled = (GetLocalDatabaseTenantsBankUrlSignatures); + + $sites = gci IIS:\Sites -Exclude "WebClientAdmin", "Eagle Eye", "Default Web Site", "IPSTS" + + $consume = $sites | % { Stop-Website -Name $_.Name; Remove-Website -Name $_.Name; } + + $iis = @{}; + if (Test-Path 'C:\Orb\Shared') { + $iis = GetOrCreateWebsite 'WebClient' 'C:\Orb\WebClient' + } else { + $iis = GetOrCreateWebsite 'WebClient' 'C:\Orb\Client' + } + + WebAdministration\Remove-WebBinding -Name $iis.Name -IPAddress * -Port 80 -ErrorAction Ignore; + + if ($toBeInstalled -ne $null) { + Write-Host "Installing new client sites under ORB\Client"; + Write-Host -NoNewline "of $($toBeInstalled.Length) we are on "; + $counter = 0; + $toBeInstalled | %{ + $counter += 1; + $newSiteBinding = $_; + $pos = $newSiteBinding.IndexOf(".") + $rightPart = $newSiteBinding.Substring($pos+1) + $cert = (Get-ChildItem cert:\LocalMachine\My | Where { $_.Subject -like "*``*.$rightPart*" } -ErrorAction Stop); + if (!(Get-WebBinding -Name 'WebClient' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding)) { + WebAdministration\New-WebBinding -Name 'WebClient' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding -SslFlags 1 + #Write-Host "Get-WebBinding -Name 'WebClient' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding"; + #Get-WebBinding -Name 'WebClient' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding + #throw; + (Get-WebBinding -Name 'WebClient' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding).AddSslCertificate($cert.Thumbprint, "my"); + } + Write-Host -NoNewline "$counter "; + } + Write-Host "done"; + } + + $iis.Start(); +} + +Function rebind-admin($rebindTier, $site) { + $toBeInstalled = (GetLocalDatabaseTenantsBankAdminUrlSignatures); + + $sites = gci IIS:\Sites -Exclude "WebClient", "Eagle Eye", "Default Web Site", "IPSTS" + + $consume = $sites | % { Stop-Website -Name $_.Name; Remove-Website -Name $_.Name; } + + $iis = @{}; + if (Test-Path 'C:\Orb\Shared') { + $iis = GetOrCreateWebsite 'WebClientAdmin' 'C:\Orb\WebClientAdmin' + } else { + $iis = GetOrCreateWebsite 'WebClientAdmin' 'C:\Orb\Admin' + } + + WebAdministration\Remove-WebBinding -Name $iis.Name -IPAddress * -Port 80 -ErrorAction Ignore; + + if ($toBeInstalled -ne $null) { + Write-Host "Installing new client sites under ORB\Admin"; + Write-Host -NoNewline "of $($toBeInstalled.Length) we are on "; + $counter = 0; + $toBeInstalled | %{ + $counter += 1; + $newSiteBinding = $_; + $pos = $newSiteBinding.IndexOf(".") + $rightPart = $newSiteBinding.Substring($pos+1) + $cert = (Get-ChildItem cert:\LocalMachine\My | Where { $_.Subject -like "*``*.$rightPart*" } -ErrorAction Stop); + if (!(WebAdministration\Get-WebBinding -Name 'WebClientAdmin' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding)) { + $binding = WebAdministration\New-WebBinding -Name 'WebClientAdmin' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding -SslFlags 1 + (WebAdministration\Get-WebBinding -Name 'WebClientAdmin' -Protocol https -Port 443 -IPAddress * -HostHeader $newSiteBinding).AddSslCertificate($cert.Thumbprint, "my"); + } + Write-Host -NoNewline "$counter "; + } + Write-Host "done"; + } +} + +Function LoadWebAdministrationModule() { + if (![Environment]::Is64BitProcess) { throw "you are using powershell in 32 bit mode. This is a failure. Please run in 64 bit mode."; } + if ((Get-Module -ListAvailable | ? { $_.Name -eq "webadministration" }) -ne $null) { + import-module WebAdministration; + } else { + throw "WebAdministration powershell module must be installed and available"; + } +} + +Function GetDeveloperTenant() { + @{ + Name = 'Developer Dynamic'; + BankGuid = '78554577-9DE6-43CD-9085-5868977156D1'; + Signature = 'developer.dev.alkamitech.com'; + AdminSignature = 'admin-developer.dev.alkamitech.com'; + DataSource = 'localhost'; + Catalog = 'DeveloperDynamic'; + Version = ''; + ConnectionString = 'data source=localhost;Integrated Security=SSPI; Database=DeveloperDynamic;Max Pool Size=500;Pooling=true;MultipleActiveResultSets=true;' + }; +} + + +#"AlkamiMaster_Dev1" +Function DoIt($site) { + Write-Host "Setting up QA env..." -ForegroundColor Yellow + if($intendedName.ToLower() -ne "dev") + { + UpdateMachineConfigs $site.Broadcaster $site.Subscription; + } + else + { + UpdateMachineConfigs "127.0.0.1" "127.0.0.1" + } + UpdateTenants $site.Database; + SetOrbHostsComplete; + LoadWebAdministrationModule; + rebind-clients $site; + rebind-admin $site; + rebind-ipsts $site; + + if (!(WebAdministration\Get-WebBinding -Name 'Default Web Site' -Protocol http -Port 80 -IPAddress * -HostHeader localhost)) { + WebAdministration\New-WebBinding -Name "Default Web Site" -Protocol http -Port 80 -IPAddress * -HostHeader localhost + } +} + +DoIt $certs[$name]; + + + + + + + +} diff --git a/Modules/Alkami.PowerShell.SDK/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.SDK/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..9dcb755 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/tools/chocolateyInstall.ps1 @@ -0,0 +1,38 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | % { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Debug "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + ## Ensure the module was able to load + Import-Module $id -Global; +} diff --git a/Modules/Alkami.PowerShell.SDK/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.SDK/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..bce6465 --- /dev/null +++ b/Modules/Alkami.PowerShell.SDK/tools/chocolateyUninstall.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.nuspec b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.nuspec new file mode 100644 index 0000000..6455181 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.nuspec @@ -0,0 +1,34 @@ + + + + Alkami.PowerShell.ServerManagement + $version$ + Alkami Platform Modules - PowerShell - ServerManagement + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.ServerManagement + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami ServerManagement module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.psd1 b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.psd1 new file mode 100644 index 0000000..a63d955 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.psd1 @@ -0,0 +1,13 @@ +@{ + RootModule = 'Alkami.PowerShell.ServerManagement.psm1' + ModuleVersion = '3.20.7' + GUID = 'f3534971-c045-49b6-934b-686a77341cfa' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing server-level items for server management.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Services','Alkami.PowerShell.Configuration' + FunctionsToExport = 'Disable-ServerManagerPopup','Disable-ServiceRecovery','Disable-UAC','Disable-WindowsFirewall','Get-FileBeatsPath','Get-RemoteFileBeatsPath','Get-RequiredServerFeaturesAndRoles','Install-7Zip','Install-FileBeats','Install-ServerFeaturesAndRoles','Set-FileBeats','Test-Ports','Test-Server' + AliasesToExport = 'Configure-FileBeats' +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.pssproj b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.pssproj new file mode 100644 index 0000000..125a6f9 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Alkami.PowerShell.ServerManagement.pssproj @@ -0,0 +1,61 @@ + + + Debug + 2.0 + {b50cf924-d8bc-43b9-9d79-edf7a121f089} + Exe + MyApplication + MyApplication + Alkami.PowerShell.ServerManagement + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.ServerManagement") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/AlkamiManifest.xml b/Modules/Alkami.PowerShell.ServerManagement/AlkamiManifest.xml new file mode 100644 index 0000000..a691ed3 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.ServerManagement + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.ServerManagement/FileBeatConfiguration/filebeat.yml b/Modules/Alkami.PowerShell.ServerManagement/FileBeatConfiguration/filebeat.yml new file mode 100644 index 0000000..529d3b0 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/FileBeatConfiguration/filebeat.yml @@ -0,0 +1,366 @@ +#=========================== Filebeat prospectors ============================= + +filebeat.inputs: + +- type: log + + enabled: true + + paths: + - C:\orblogs\*.slog + + fields: + serverRole: {role} + serverGroup: '{group}' + timeZone: {timezone} + fields_under_root: true + close_renamed: true + +- type: log + + enabled: true + + # Paths that should be crawled and fetched. Glob based paths. + paths: + - C:\orblogs\*.log* + + # Custom fields instrumented by the installation script + fields: + serverRole: {role} + serverGroup: '{group}' + timeZone: {timezone} + fields_under_root: true + + exclude_lines: ['\|\*\| (BrokerMessages|Alkami\.(Trace(d)?Sql|App\.Common\.Data\.Dao\.HibernateBankDao|Broker\.(App|ZeroMQ)|Client\.Services\.Bank\.Repository|Services\.Subscriptions\.ParticipatingClient\.SelfResolvingClient|MicroServices\.(Authorization\.Service\.AuthorizationServiceImp|TracedMessages))) \|\*\|'] + + # Exclude microservices + exclude_files: + [(.*)] + + ### Multiline options + multiline.pattern: '^\d{4}\-' + multiline.negate: true + multiline.match: after + close_renamed : true + +- type: log + + enabled: true + + # Paths that should be crawled and fetched. Glob based paths. + paths: + - C:\orblogs\Alkami.Admin.WebClient* + - C:\orblogs\Alkami.Api.AFX* + - C:\orblogs\Alkami.Api.CUFX* + - C:\orblogs\Alkami.Api.ORBFX* + - C:\orblogs\Alkami.Api.TextBanking* + - C:\orblogs\Alkami.App.Audit.Host.log* + - C:\orblogs\Alkami.App.Bank.Host.BillPay.log* + - C:\orblogs\Alkami.App.Bank.Host.OSI.log* + - C:\orblogs\Alkami.App.Bank.Host.Symconnect.log* + - C:\orblogs\Alkami.App.Bank.Host.log* + - C:\orblogs\Alkami.App.Content.Host.log* + - C:\orblogs\Alkami.App.Core.Host.log* + - C:\orblogs\Alkami.App.Exception.Host.log* + - C:\orblogs\Alkami.App.MessageCenter.Host.log* + - C:\orblogs\Alkami.App.Nag.WindowsService.log* + - C:\orblogs\Alkami.App.NagConfiguration.Host.log* + - C:\orblogs\Alkami.App.Notification.Host.log* + - C:\orblogs\Alkami.App.Radium.WindowsService.log* + - C:\orblogs\Alkami.App.STSConfiguration.Host.log* + - C:\orblogs\Alkami.App.Scheduler.Host.log* + - C:\orblogs\Alkami.App.Security.RPSTS.Host.log* + - C:\orblogs\Alkami.App.SecurityManagement.Host.log* + - C:\orblogs\Alkami.App.SymConnectMultiplexer.Host.log* + - C:\orblogs\Alkami.Client.VisaNotificationCallback.Host.log* + - C:\orblogs\Alkami.Client.VisaRegistration.Host.log* + - C:\orblogs\Alkami.Security.IPSTS* + - C:\orblogs\Alkami.WebClient* + - C:\orblogs\Alkami.MicroServices.AccountBalanceSync1.Processor.Host.log* + - C:\orblogs\Alkami.MicroServices.AccountBalanceSync2.Processor.Host.log* + - C:\orblogs\Alkami.MicroServices.Accounts.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.AchTemplates.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ACHTransfersRequests.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.AddressValidation.Correio.Host.log* + - C:\orblogs\Alkami.MicroServices.AddressValidation.SmartyStreets.Host.log* + - C:\orblogs\Alkami.MicroServices.Aggregation.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.AggregationProviders.Yodlee.Host.log* + - C:\orblogs\Alkami.MicroServices.Alerts.Patelco.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Alerts.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ApplicationSettings.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Audit.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Authentication.AD.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Authorization.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.AutoBiller.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.AutoBiller.Symitar.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BCUBenefits.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPay.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.CheckFree.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.CheckFreeSB.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.FIS.MACU.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.FIS.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.IPay.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BillPayProviders.Payveris.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Broker.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessAchConfiguration.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessACHProcessing.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessEvents.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessReport.Notifications.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessReportsData.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.BusinessReportVariation.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagement.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Bcu.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Connex.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Corelation.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.DNA.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Ondot.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Pscu.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Spectrum.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Static.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.SymConnect.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.UltraData.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.Universal.Host.log* + - C:\orblogs\Alkami.MicroServices.CardManagementProviders.XP.Host.log* + - C:\orblogs\Alkami.MicroServices.CheckOrderProviders.Deluxe.Host.log* + - C:\orblogs\Alkami.MicroServices.CivicRewards.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CMS.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Contacts.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.AccountUpdate.CoreApi.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.AccountUpdate.ESB.OCCU.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.AccountUpdate.Miser.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.AccountUpdate.Spectrum.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.AccountUpdate.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.CardManagement.Spectrum.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.CCM.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.CoreApi.Registration.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.CoreApi.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.ESB.OCCU.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.Miser.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.Registration.ESB.OCCU.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.Registration.Miser.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.RTA.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Core.Spectrum.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.Corelation.Host.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.ESB.OTS.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.Spectrum.Host.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.Spectrum.Requests.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.CourtesyPayProviders.SymConnect.Service.Requests.log* + - C:\orblogs\Alkami.MicroServices.CustomCore.Miser.Occu.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.BlockedACH.Alert.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.ExternalTransfer.Alert.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.ExternalTransferCancelled.Alert.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.TransferFailed.Alert.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.TransferSucceeded.Alert.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.TransferFailed.Host.log* + - C:\orblogs\Alkami.MicroServices.EM.TransferSucceeded.Host.log* + - C:\orblogs\Alkami.MicroServices.EStatements.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.Cleanup.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.ETAP.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.MMAP.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.OAP.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.SAP.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.EventManagement.VAP.Host.log* + - C:\orblogs\Alkami.MicroServices.ExtendedProperties.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Fact.Miser.Occu.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Features.Beacon.Host.log* + - C:\orblogs\Alkami.MicroServices.Features.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.FicoScore.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.FileGeneration.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.FluxManagement.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.FluxReporting.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.FluxSecurity.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Forms.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Ftp.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Holidays.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ICCUBusinessCardManagement.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Images.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Limits.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.LoanPayment.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.MessageCenter.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.MyAccounts.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.NachaFileGenerator.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Notifications.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ODPP.Corelation.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ODPP.Patelco.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ODPP.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.ODPP.UltraData.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Payments.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Payroll.SandiaLabs.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Payroll.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Processor.ApiPassthrough.Host.log* + - C:\orblogs\Alkami.MicroServices.Processor.Flux.Reporting.Host.log* + - C:\orblogs\Alkami.MicroServices.Processor.RemoteDepositCompletion.Host.log* + - C:\orblogs\Alkami.MicroServices.Processor.RIMT.Host.log* + - C:\orblogs\Alkami.MicroServices.QBO.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.DNA.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.Dynamic.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.ESB.Desert.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.Esb.Occu.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.MeridianLink.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.Miser.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.Spectrum.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.UltraData.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.QuickApply.XP.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RDCoreDeposit.Phoenix.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RDCoreDeposit.Symitar.Service.Requests.log* + - C:\orblogs\Alkami.MicroServices.Registration.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RememberedDevices.Entrust.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RemoteDeposit.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RemoteDepositProviders.Ensenta.Requests.log* + - C:\orblogs\Alkami.MicroServices.RemoteDepositProviders.Ensenta.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RemoteDepositProviders.Fxd.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.RemoteDepositProviders.Vertifi.Requests.log* + - C:\orblogs\Alkami.MicroServices.RemoteDepositProviders.Vertifi.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Reporting.BusinessReports.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Reporting.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Reporting.URA.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Reports.Export.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Risk.Alkami.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Risk.DetectTA.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Risk.Entrust.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Risk.Management.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Rules.DataSheet.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Rules.Package.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Rules.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SamlAuth.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Security.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Settings.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SiteText.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SkipPaymentProviders.Corelation.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SkipPaymentProviders.Patelco.Host.log* + - C:\orblogs\Alkami.MicroServices.SkipPaymentProviders.SymConnect.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SkipPaymentProviders.Veridian.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.AutoBooks.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.Avoka.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.BALoyaltyRewards.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.ClickSwitch.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.CubusSkipAPay.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.Imsi.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.InstantOpen.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.MeridianLink.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.MeridianLinkSsoV2.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.MyCardInfo.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.OnDot.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.SymApp.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.TCILoanOrigination.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.UChooseRewards.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.uOpen.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SSOProviders.VirtualCapture.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.SymConnectMultiplexer.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.TIMicro.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Transfers.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.TransferShim.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.TransfersOrchestration.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.Transactions.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.UserInterface.Service.Host.log* + - C:\orblogs\Alkami.MicroServices.WebAnalytics.Adobe.EP.Service.Host.log* + - C:\orblogs\Alkami.MS.LocationProviders.COOP.Host.log* + - C:\orblogs\Alkami.MS.LocationProviders.COOP.ProximitySearch.Host.log* + - C:\orblogs\Alkami.MS.LocationProviders.Dynamic.Host.log* + - C:\orblogs\Alkami.MS.LocationProviders.LocatorSearch.Host.log* + - C:\orblogs\Alkami.MS.LocationProviders.Patelco.Host.log* + - C:\orblogs\Alkami.MS.Processor.MessageCenter.SLA.Service.Host.log* + - C:\orblogs\Alkami.Ops.SmokeTest.log* + - C:\orblogs\Alkami.Services.Subscriptions.Host.log* + - C:\orblogs\Achieva.MS.Integrations.Service.Host.log* + - C:\orblogs\BL.MS.AccountOpening.Service.Host.log* + - C:\orblogs\OTS.MS.LoanAppPreFill.Service.Host.log* + - C:\orblogs\BCU.MS.SalesforceProvider.Service.Host.log* + - C:\orblogs\BFCU.MS.AccountOpening.Service.Host.log* + - C:\orblogs\CAPED.MS.InterstitialService.Service.Host.log* + - C:\orblogs\CAPED.MS.MemberServices.Service.Host.log* + - C:\orblogs\CAPED.MS.QuickApply.Service.Host.log* + - C:\orblogs\CAPED.MS.TravelNotes.Service.Host.log* + - C:\orblogs\Connexus.MS.ActonConnector.Service.Host.log* + - C:\orblogs\Connexus.MS.ContentFetch.Service.Host.log* + - C:\orblogs\Connexus.MS.FormsHandler.Service.Host.log* + - C:\orblogs\CRNGCU.MS.RteSvc.Service.Host.log* + - C:\orblogs\CRNGCU.MS.WS.Cards.Service.Host.log* + - C:\orblogs\CU4Kids.MS.GiveworxApi.Service.Host.log* + - C:\orblogs\OTS.MS.LoanAppPreFill.Service.Host.log* + - C:\orblogs\DS.AccountService.MS.Service.Host.log* + - C:\orblogs\DS.BranchService.MS.Service.Host.log* + - C:\orblogs\DS.CMN.MS.Service.Host.log* + - C:\orblogs\DS.CopyRequestService.MS.Service.Host.log* + - C:\orblogs\DS.CoreService.MS.Service.Host.log* + - C:\orblogs\DS.DocumentRepository.MS.Service.Host.log* + - C:\orblogs\DS.EStatements.MS.Service.Host.log* + - C:\orblogs\DS.MarketingEmailService.MS.Service.Host.log* + - C:\orblogs\DS.MicroService.ContentService.Service.Host.log* + - C:\orblogs\DS.RepositoryService.MS.Service.Host.log* + - C:\orblogs\DS.SecureMessage.MS.Service.Host.log* + - C:\orblogs\DS.SecurityService.MS.Service.Host.log* + - C:\orblogs\DS.Settings.MS.Service.Host.log* + - C:\orblogs\DS.SkipAPayService.MS.Service.Host.log* + - C:\orblogs\Desert.MS.AuditService.Service.Host.log* + - C:\orblogs\Desert.MS.LocationService.Service.Host.log* + - C:\orblogs\Desert.MS.TravelNotificationService.Service.Host.log* + - C:\orblogs\ECU.MS.CardManagement.DataCard.Host.log* + - C:\orblogs\ECU.MS.MemberLoyalty.Service.Host.log* + - C:\orblogs\FFMFCU.MS.DocuSign.Service.Host.log* + - C:\orblogs\FFMFCU.MS.MATLABProductionServer.Service.Host.log* + - C:\orblogs\FFMFCU.MS.PDFTool.Service.Host.log* + - C:\orblogs\FFCU.MS.HolidayLoan.Service.Host.log* + - C:\orblogs\FPCU.MS.Nexus.Service.Host.log* + - C:\orblogs\FTFCU.MS.ManageCard.Service.Host.log* + - C:\orblogs\FTFCU.MS.P2P.Service.Host.log* + - C:\orblogs\FTFCU.MS.Products.Service.Host.log* + - C:\orblogs\FTFCU.MS.Risk.Service.Host.log* + - C:\orblogs\FTFCU.MS.Wires.Service.Host.log* + - C:\orblogs\GFFCU.MS.GetQuickApplyCategories.Service.Host.log* + - C:\orblogs\MACU.MS.CardSwap.Service.Host.log* + - C:\orblogs\MACU.MS.CreditLock.Service.Host.log* + - C:\orblogs\Mfcu.MS.QuickApply.Service.Host.log* + - C:\orblogs\OCCU.MS.CardManagement.Host.log* + - C:\orblogs\OCCU.MS.PriorityCash.Service.Host.log* + - C:\orblogs\PAYV.MS.BillPay.Service.Host.log* + - C:\orblogs\Patelco.MS.LoanPayoffService.Service.Host.log* + - C:\orblogs\SECU.MS.AccountOpening.Service.Host.log* + - C:\orblogs\SECU.MS.LoanFlex.Service.Host.log* + - C:\orblogs\USBFCU.MS.Fuego.Service.Host.log* + - C:\orblogs\SMCU.MS.CourtesyPayV2.Service.Host.log* + - C:\orblogs\STCU.OnlineBanking.MS.Bal.Service.Host.log* + - C:\orblogs\STCU.OnlineBanking.MS.Savvy.Service.Host.log* + - C:\orblogs\STCU.OnlineBanking.MS.Ssom.Service.Host.log* + - C:\orblogs\STCU.OnlineBanking.MS.StatusHub.Service.Host.log* + - C:\orblogs\VCU.MS.CardCentral.Service.Host.log* + - C:\orblogs\VCU.MS.CardControl.Notifications.Host.log* + - C:\orblogs\VCU.MS.CardControl.Service.Host.log* + - C:\orblogs\VCU.MS.SavvyMoney.Service.Host.log* + + # Custom fields instrumented by the installation script + fields: + serverRole: {role} + serverGroup: '{group}' + timeZone: {timezone} + fields_under_root: true + + exclude_lines: ['\|\*\| (BrokerMessages|Alkami\.(Trace(d)?Sql|App\.Common\.Data\.Dao\.HibernateBankDao|Broker\.(App|ZeroMQ)|Client\.Services\.Bank\.Repository|Services\.Subscriptions\.ParticipatingClient\.SelfResolvingClient|MicroServices\.(Authorization\.Service\.AuthorizationServiceImp|TracedMessages))) \|\*\|'] + # Exclude Nothing + exclude_files: + + ### Multiline options + multiline.pattern: '^\d{4}\-' + multiline.negate: true + multiline.match: after + close_renamed : true + +processors: +- drop_fields: + fields: ["host"] + +#================================ Outputs ===================================== +#----------------------------- Logstash output -------------------------------- +output.logstash: + # The Logstash hosts + # This value should be modified during install to point to the logstash dns, with the port for the appropriate environment + ssl.verification_mode: none + ssl.enabled: true + ssl.supported_protocols: [TLSv1.2] + loadbalance: true + hosts: ["orb.os-logstash.haystack.prod.alkami.net:PORT"] diff --git a/Modules/Alkami.PowerShell.ServerManagement/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServerManagerPopup.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServerManagerPopup.ps1 new file mode 100644 index 0000000..455df34 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServerManagerPopup.ps1 @@ -0,0 +1,19 @@ +function Disable-ServerManagerPopup { +<# +.SYNOPSIS + Disables the Server Manager Popup +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName) + + $registryPath = "HKLM:\Software\Microsoft\ServerManager" + $keyName = "DoNotOpenServerManagerAtLogon" + $desiredValue = "1" + + (Set-RegistryValue $registryPath $keyName $desiredValue) | Out-Null + + Write-Verbose "$logLead : Registry value set to disable server manager popup." +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServiceRecovery.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServiceRecovery.ps1 new file mode 100644 index 0000000..e505943 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-ServiceRecovery.ps1 @@ -0,0 +1,37 @@ +function Disable-ServiceRecovery { +<# +.SYNOPSIS + Configures Recovery Options to Never Restart Automatically +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string[]]$serviceNames = (Get-AlkamiServices | Select-Object -ExpandProperty Name) + ) + + $logLead = (Get-LogLeadName); + + Write-Verbose ("$logLead : Found {0} Services" -f $serviceNames.Count) + + foreach ($service in $serviceNames) { + Write-Output ("$logLead : Updating service {0}" -f $service) + + Write-Output "$logLead : * Setting Recovery Options to 0" + $scOutput = sc.exe failure $service reset= 86400 actions= ""/0/""/0/""/0 + + if ($LASTEXITCODE -ne 0) { + Write-Warning ("$logLead : * An error occurred setting recovery options for service {0}" -f $service) + $scOutput | ForEach-Object { Write-Output ("$logLead : * {0}" -f $_)} + } + + Write-Output "$logLead : * Setting failureFlag to 0" + $scOutput = sc.exe failureflag $service 0 + + if ($LASTEXITCODE -ne 0) { + Write-Warning ("$logLead : * An error occurred setting the failure flag option for service {0}" -f $service) + $scOutput | ForEach-Object { Write-Output ("$logLead : * {0}" -f $_)} + } + } +} + diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-UAC.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-UAC.ps1 new file mode 100644 index 0000000..6d8ff0d --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-UAC.ps1 @@ -0,0 +1,22 @@ +function Disable-UAC { +<# +.SYNOPSIS + Disables UAC/LUA By Modifying the Registry. A reboot is required for the change to take effect +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName); + $itemProp = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableLUA" + + if ($itemProp.EnableLUA -eq 0) { + Write-Output ("$logLead : The EnableLUA DWORD is Already Set to 0 -- no changes required") + return + } + + Write-Output ("$logLead : Setting the EnableLUA DWORD Value to 0") + Write-Warning ("$logLead : A Reboot is Required for the Change to Take Effect!") + Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableLUA" -Value "0" +} + diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-WindowsFirewall.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-WindowsFirewall.ps1 new file mode 100644 index 0000000..489f130 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Disable-WindowsFirewall.ps1 @@ -0,0 +1,19 @@ +function Disable-WindowsFirewall { +<# +.SYNOPSIS + Disables the Windows Firewall +#> + + [CmdletBinding()] + param() + + $logLead = (Get-LogLeadName); + Write-Output "$logLead : Disabling the Windows Firewall" + + $result = netsh firewall set opmode disable + + if (!([String]::IsNullOrEmpty($result))) { + Write-Verbose ("$logLead : {0}" -f $result) + } +} + diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Get-FileBeatsPath.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-FileBeatsPath.ps1 new file mode 100644 index 0000000..9bc7954 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-FileBeatsPath.ps1 @@ -0,0 +1,45 @@ +function Get-FileBeatsPath { +<# +.SYNOPSIS + Get the local installed folder path(s) for Filebeat. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + ) + + $logLead = (Get-LogLeadName) + + # Define the known paths for easier validation + $deprecatedPath1 = "C:\Tools\Beats\FileBeat\filebeat-6.8.13-windows-x86_64" + $deprecatedPath2 = "C:\Tools\Beats\FileBeat\filebeat-6.1.2-windows-x86_64" + $expectedPath = "C:\Tools\Beats\FileBeat\tools" + $opensearchPath = "C:\Tools\Beats\FileBeat_os\tools" + + # Should be installed in either the expectedPath or one of the deprecatedPaths + $possibleSearchLocations = @($expectedPath, $deprecatedPath1, $deprecatedPath2) + + # We may have more than one instance of FileBeats installed on a given server + $returnPaths = @() + + if (Test-Path $opensearchPath) { + $returnPaths += $opensearchPath + } + + foreach ($path in $possibleSearchLocations) { + if (Test-Path $path) { + $returnPaths += $path + + # Only take the first path found, if at all + break + } + } + + if (Test-IsCollectionNullOrEmpty $returnPaths) { + Write-Warning "$logLead : Filebeats is not installed on $env:COMPUTERNAME" + # maintain legacy consistency by returning $null + $returnPaths = $null + } + + return $returnPaths +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RemoteFileBeatsPath.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RemoteFileBeatsPath.ps1 new file mode 100644 index 0000000..277da9d --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RemoteFileBeatsPath.ps1 @@ -0,0 +1,26 @@ +function Get-RemoteFileBeatsPath { + <# +.SYNOPSIS + Get the installed folder path of filebeat for a given server. + +.PARAMETER ComputerName + Server to get the path from +#> + [CmdletBinding()] + Param( + [Alias("Server")] + [string]$ComputerName + ) + + if (!$ComputerName) { + Write-Error "No remote computer name was provided. Did you mean to call Get-FileBeatsPath?" + } else { + + + $scriptBlock = { + Get-FileBeatsPath + } + + return Invoke-Command -ScriptBlock $scriptBlock -ComputerName $ComputerName + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.ps1 new file mode 100644 index 0000000..80d9584 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.ps1 @@ -0,0 +1,32 @@ +function Get-RequiredServerFeaturesAndRoles { +<# +.SYNOPSIS + Returns a hashtable array of tiered server features and roles used for server bootstrapping +#> + [CmdletBinding()] + [OutputType([System.Object[]])] + Param() + + $logLead = (Get-LogLeadName) + + # Initialize the result to Windows Features that are common between Windows Server and Windows Server Core. + $result = @( + @{"Name"="Tier1"; "Features"=@("Web-Server","WAS","Web-Client-Auth","Web-Cert-Auth","Web-Mgmt-Tools","Web-Http-Redirect","Web-Custom-Logging","Web-Log-Libraries");}, + @{"Name"="Tier2"; "Features"=@("Web-Request-Monitor","Web-Http-Tracing","Web-Stat-Compression","Web-Basic-Auth","Web-IP-Security","Web-Url-Auth","Web-Windows-Auth","Web-ASP","Web-Asp-Net");}, + @{"Name"="Tier3"; "Features"=@("Web-ISAPI-Ext","Web-ISAPI-Filter","Web-Mgmt-Compat","Web-Scripting-Tools","Web-Mgmt-Service");}, + @{"Name"="Tier4"; "Features"=@("NET-HTTP-Activation","NET-Non-HTTP-Activ","NET-WCF-HTTP-Activation45","NET-WCF-TCP-Activation45","WAS-NET-Environment","Web-Dyn-Compression","Web-CGI","Web-Includes","Web-Lgcy-Scripting","Web-WMI","RSAT-AD-PowerShell","Telnet-Client","Web-WebSockets");} + ) + + if ( Test-IsWindowsServerCore ) { + + Write-Verbose "$logLead : Detected Windows Server Core OS; omitting Features that are not supported." + + } else { + + # Inject Windows Server Features that are not present on Windows Server Core. + $result[0].Features += "Windows-Identity-Foundation" + $result[3].Features += "Web-Lgcy-Mgmt-Console" + } + + return $result +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.tests.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.tests.ps1 new file mode 100644 index 0000000..b72464a --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Get-RequiredServerFeaturesAndRoles.tests.ps1 @@ -0,0 +1,51 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-RequiredServerFeaturesAndRoles" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "Get-RequiredServerFeaturesAndRoles.tests" } + + Context "Results" { + + It "Returns Windows-Identity-Foundation on Windows Server" { + + Mock -CommandName Test-IsWindowsServerCore -ModuleName $moduleForMock -MockWith { return $false } + + ( Get-RequiredServerFeaturesAndRoles ).Features | Should -Contain 'Windows-Identity-Foundation' + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServerCore -Scope It -Exactly 1 + } + + It "Returns Web-Lgcy-Mgmt-Console on Windows Server" { + + Mock -CommandName Test-IsWindowsServerCore -ModuleName $moduleForMock -MockWith { return $false } + + ( Get-RequiredServerFeaturesAndRoles ).Features | Should -Contain 'Web-Lgcy-Mgmt-Console' + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServerCore -Scope It -Exactly 1 + } + + It "Does Not Return Windows-Identity-Foundation on Windows Server Core" { + + Mock -CommandName Test-IsWindowsServerCore -ModuleName $moduleForMock -MockWith { return $true } + + ( Get-RequiredServerFeaturesAndRoles ).Features | Should -Not -Contain 'Windows-Identity-Foundation' + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServerCore -Scope It -Exactly 1 + } + + It "Does Not Return Web-Lgcy-Mgmt-Console on Windows Server Core" { + + Mock -CommandName Test-IsWindowsServerCore -ModuleName $moduleForMock -MockWith { return $true } + + ( Get-RequiredServerFeaturesAndRoles ).Features | Should -Not -Contain 'Web-Lgcy-Mgmt-Console' + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServerCore -Scope It -Exactly 1 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Install-7Zip.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-7Zip.ps1 new file mode 100644 index 0000000..ec2928e --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-7Zip.ps1 @@ -0,0 +1,50 @@ +function Install-7Zip { +<# +.SYNOPSIS + +Installs 7-Zip and Adds to the System Path if Needed +#> + + [CmdletBinding()] + Param( + # 7zip doesn't provide a static 'latest stable' download link, so this will require occasional maintenance + [Parameter(Mandatory = $false)] + [string]$zipDownloadLink = "http://www.7-zip.org/a/7z1701-x64.msi" + ) + + $logLead = (Get-LogLeadName); + Write-Output ("$logLead : Downloading 7Zip from {0}" -f $zipDownloadLink) + + $msiName = "7Zip.msi" + $folderDateFormat = Get-Date -Format ddMMyyyy + $outputFolder = ("C:\Temp\Deploy\7Zip_{0}" -f $folderDateFormat) + + if (!(Test-Path $outputFolder)) { + Write-Verbose ("$logLead : Creating 7-Zip Download Folder {0}" -f $outputFolder) + New-Item $outputFolder -ItemType Directory | Out-Null + } + + $downloadedMSI = (Join-Path $outputFolder $msiName) + if (Test-Path $downloadedMSI) { + # If a job is rerun, this should prevent downloading the agent again, unless the agent spans a day + Write-Output ("$logLead : Using Existing MSI from {0}" -f $downloadedMSI) + } + else { + Write-Output ("$logLead : Downloading 7-Zip Installer from {0} to {1}" -f $zipDownloadLink, $downloadedMSI) + Invoke-WebRequest -Uri $zipDownloadLink -OutFile $downloadedMSI -UseBasicParsing + } + + $installParams = ("/i {0} /qn /norestart" -f $downloadedMSI) + Write-Verbose ("$logLead : Running msiexec.exe with parameters {0}" -f $installParams) + + $installProcess = Start-Process msiexec.exe -ArgumentList $installParams -NoNewWindow -PassThru -Wait + Write-Output ("$logLead : Installation completed with exit code {0}" -f $installProcess.ExitCode) + + $expectedInstallPath = "C:\Program Files\7-Zip" + if (!(Test-Path $expectedInstallPath)) { + Write-Warning ("$logLead : Unable to find expected path {0}. Verify the installation was successful and update the path environmental variable as appropriate." -f $expectedInstallPath) + return + } + + Add-DirectoryToPath $expectedInstallPath +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Install-FileBeats.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-FileBeats.ps1 new file mode 100644 index 0000000..0ffc8f6 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-FileBeats.ps1 @@ -0,0 +1,132 @@ +function Install-FileBeats { +<# +.SYNOPSIS + Installs FileBeats. May download a version if not present already. + +.PARAMETER ChocoSource + [Optional] The chocolatey feed to look for the FileBeat package at if not locally present. Defaults to the SRETools repo as preferred. + +.PARAMETER OutputFolder + The location to download the file to if not found. + +.PARAMETER TargetFolder + [Optional] The location where this service is found. Can be located with Get-FileBeatsPath (which may return one or more paths, however). Defaults to "C:\Tools\Beats\FileBeat" + +.PARAMETER Port + The port this server runs against for reporting. Valid values approx 5046-5048 varying. + +.PARAMETER FileBeatServiceName + [Optional] The name of the service being installed. Defaults to "Filebeat (Haystack)" but could also be "Filebeat_os (Haystack)" etc. + +.PARAMETER ServerGroupName + Configure the server group this server reports for. Ex: production pod information + +.PARAMETER ServerRoleName + Configure the role usage of this server. Examples: ORB, TI, etc. + +.PARAMETER FileBeatVersion + [Optional] Specify the preferred version to be installed. Defaults to 6.8.13 +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("Source")] + [string]$ChocoSource = "https://packagerepo.orb.alkamitech.com/nuget/SRETools/", + + [Parameter(Mandatory = $false)] + [Alias("DownloadPath")] + [string]$OutputFolder, + + [Parameter(Mandatory = $false)] + [Alias("InstallFolder")] + [string]$TargetFolder = "C:\Tools\Beats\FileBeat_os", + + [Parameter(Mandatory = $false)] + [Alias("LogstashPort")] + [int]$Port, + + [Parameter(Mandatory = $false)] + [Alias("ServiceName")] + [string]$FileBeatServiceName = "Filebeat_os (Haystack)", + + [Parameter(Mandatory = $false)] + [Alias("ServerGroup")] + [string]$ServerGroupName, + + [Parameter(Mandatory = $false)] + [Alias("ServerRole")] + [string]$ServerRoleName, + + [Parameter(Mandatory = $false)] + $FileBeatVersion = "6.8.13" + ) + + $logLead = (Get-LogLeadName); + + $fileBeatService = Get-Service -Name $FileBeatServiceName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + + #Set $outputFolder default + if ([string]::IsNullOrEmpty($OutputFolder)) { + $chocoInstallPath = Get-ChocolateyInstallPath + $OutputFolder = Join-Path $chocoInstallPath "lib\filebeat" + } + + if ($null -ne $fileBeatService -and $fileBeatService.CanStop) { + Write-Output ("$logLead : Stopping the $FileBeatServiceName Service") + $fileBeatService.Stop() | Out-Null + } + + # TODO: Wrap this in an invokable function so we can unit-test this file + choco install filebeat -y --source $ChocoSource --version $FileBeatVersion + + $toolsFolder = Get-ChildItem $outputFolder | Where-Object { $_.PsIsContainer } | Select-Object -First 1 + + if (Test-Path $TargetFolder) { + Write-Host "$logLead : Using Existing FileBeat Folder at [$TargetFolder]" + } else { + New-Item $TargetFolder -ItemType Directory -Force | Out-Null + } + + Write-Host "$logLead : Copying $($toolsFolder.Name) to $TargetFolder" + # For some reason I'm getting file locks so whatever, I'm copying it + Copy-Item -Recurse $toolsFolder.FullName $TargetFolder -Force + + $installDir = (Join-Path $TargetFolder ($toolsFolder.Name)) + + $fileBeatService = Get-Service -Name $FileBeatServiceName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + + if ($null -eq $fileBeatService) { + Write-Host "$logLead : Installing $FileBeatServiceName Service" + + $exePath = (Join-Path -Path $installDir -ChildPath "filebeat.exe") + $ymlPath = (Join-Path -Path $installDir -ChildPath "filebeat.yml") + $dataPath = (Join-Path -Path $installDir -ChildPath "data") + $logsPath = (Join-Path -Path $installDir -ChildPath "logs") + $binaryPathNameField = "'$exePath' -c '$ymlPath' -path.home '$installDir' -path.data '$dataPath' -path.logs '$logsPath'" + # Convert the single quotes above to double quotes + $binaryPathNameField = $binaryPathNameField -replace "'",'"' + + $newServiceSplat = @{ + Name = $FileBeatServiceName + DisplayName = $FileBeatServiceName + BinaryPathName = $binaryPathNameField + } + (New-Service @newServiceSplat) | Out-Null + } + + # Should not error out, because we just installed it if it wasn't installed. + $fileBeatService = (Get-Service -Name $FileBeatServiceName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) + + if ($null -ne $fileBeatService) { + if (($null -ne $Port) -and ($Port -gt 0)) { + $configPath = (Get-ChildItem $targetFolder -Recurse -Include "filebeat.yml") | Select-Object -First 1 + Set-FileBeats -LogstashPort $Port -TargetConfigPath $configPath.FullName -ServerGroup $ServerGroupName -ServerRole $ServerRoleName + } + + Write-Host ("$logLead : Starting $FileBeatServiceName Service") + Start-FileBeatsService -ServiceName $FileBeatServiceName + } else { + Write-Warning ("$logLead : Could not Find the $FileBeatServiceName Service") + return + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.ps1 new file mode 100644 index 0000000..38fe4a3 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.ps1 @@ -0,0 +1,58 @@ +function Install-ServerFeaturesAndRoles { +<# +.SYNOPSIS + Installs the features and roles required to run and maintain ORB +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param() + + $logLead = (Get-LogLeadName) + $restartRequired = $false + + Write-Host "$logLead : Preparing to Install Windows Features" + + $features = (Get-RequiredServerFeaturesAndRoles) + + if (Test-IsWindowsServer) { + + Write-Host "$logLead : Server OS Detected. Importing ServerManager Module" + Import-Module ServerManager | Out-Null + } + + foreach ($featureSet in $features) { + + Write-Verbose "$logLead : Preparing to install feature set $($featureSet.Name)" + foreach ( $feature in $featureSet.Features ) { + + if ( $null -eq ( Get-WindowsFeature -Name $feature ) ) { + + Write-Warning "$logLead : Feature set $($featureSet.Name) contains feature $feature that does not exist; skipping." + Write-Warning "Review the results before continuing with the installation!" + + } else { + + $result = (Install-WindowsFeature -Name $feature -ErrorAction SilentlyContinue -Verbose:$false) + + Write-Verbose "$logLead : $($featureSet.Name) : $feature : Exit code is $($result.ExitCode), RestartRequired is $($result.RestartNeeded)" + + if ($result.ExitCode -notin [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::Success, ` + [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::NoChangeNeeded, ` + [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::SuccessRestartRequired) { + + # Exit early on failure + Write-Error "$logLead : $($featureSet.Name) feature $feature failed to install with exit code $($result.ExitCode)." + return $false + + } elseif ($result.RestartNeeded -ne [Microsoft.Windows.ServerManager.Commands.RestartState]::No) { + + # Log that a restart is required but go ahead and install the rest + Write-Warning "$logLead : A restart is required to complete installation of $($featureSet.Name) $feature" + $restartRequired = $true + } + } + } + } + + return ($restartRequired -eq $false) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.tests.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.tests.ps1 new file mode 100644 index 0000000..bb8a63c --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Install-ServerFeaturesAndRoles.tests.ps1 @@ -0,0 +1,278 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +# Import the ServerManager module for function definitions necessary to mock. +if (Test-IsWindowsServer) { + + Import-Module ServerManager | Out-Null +} + +Describe "Install-ServerFeaturesAndRoles" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "Install-ServerFeaturesAndRoles.tests" } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Import-Module -MockWith {} + + Context "Results" { + + It "Imports ServerManager Module on Windows Server" { + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + + Install-ServerFeaturesAndRoles | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Import-Module -Scope It -Exactly 1 ` + -ParameterFilter { $Name -match "ServerManager" } + } + + It "Does Not Import ServerManager Module on Windows Client" { + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $false } + + Install-ServerFeaturesAndRoles | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Import-Module -Scope It -Exactly 0 ` + -ParameterFilter { $Name -match "ServerManager" } + } + + It "Writes Warning and Skips if Feature Does Not Exist" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $null } + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { return $null } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ Name = 'TestSet'; Features = @( 'TestFeature1', 'TestFeature2' ) } + ) + } + + Install-ServerFeaturesAndRoles | Should -BeTrue + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 0 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 1 ` + -ParameterFilter { $Message -match "Feature set TestSet contains feature TestFeature1 that does not exist" } + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Warning -Scope It -Exactly 1 ` + -ParameterFilter { $Message -match "Feature set TestSet contains feature TestFeature2 that does not exist" } + } + + It "Validates Existence of Each Feature in Feature Set" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet' + Features = @( + 'TestFeature1', + 'TestFeature2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::Success + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::No + } + } + + Install-ServerFeaturesAndRoles | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 2 + } + + It "Writes Error and Aborts if Feature Set Installation Fails" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet' + Features = @( + 'TestFeature1', + 'TestFeature2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::ArgumentNotValid + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::No + } + } + + Install-ServerFeaturesAndRoles | Should -BeFalse + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Scope It -Exactly 1 ` + -ParameterFilter { $Message -match "TestSet feature TestFeature1 failed to install" } + } + + It "Installs All Features In A Set" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet' + Features = @( + 'TestFeature1', + 'TestFeature2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::Success + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::No + } + } + + Install-ServerFeaturesAndRoles | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Scope It -Exactly 0 + } + + It "Installs All Features In All Sets" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet1' + Features = @( + 'TestFeature1_1', + 'TestFeature1_2' + ) + }, + @{ + Name = 'TestSet2' + Features = @( + 'TestFeature2_1', + 'TestFeature2_2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::Success + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::No + } + } + + Install-ServerFeaturesAndRoles | Out-Null + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 4 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 4 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Scope It -Exactly 0 + } + + It "Returns False if Feature Requires Reboot" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet' + Features = @( + 'TestFeature1', + 'TestFeature2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::SuccessRestartRequired + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::Yes + } + } + + Install-ServerFeaturesAndRoles | Should -BeFalse + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Scope It -Exactly 0 + } + + It "Returns True if Feature Does Not Require Reboot" { + + Mock -CommandName Test-IsWindowsServer -ModuleName $moduleForMock -MockWith { return $true } + Mock -CommandName Get-WindowsFeature -ModuleName $moduleForMock -MockWith { return $true } + + Mock -CommandName Get-RequiredServerFeaturesAndRoles -ModuleName $moduleForMock -MockWith { + return @( + @{ + Name = 'TestSet' + Features = @( + 'TestFeature1', + 'TestFeature2' + ) + } + ) + } + + Mock -CommandName Install-WindowsFeature -ModuleName $moduleForMock -MockWith { + return @{ + ExitCode = [Microsoft.Windows.ServerManager.Commands.FeatureOperationExitCode]::Success + RestartNeeded = [Microsoft.Windows.ServerManager.Commands.RestartState]::No + } + } + + Install-ServerFeaturesAndRoles | Should -BeTrue + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-RequiredServerFeaturesAndRoles -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-IsWindowsServer -Scope It -Exactly 1 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Get-WindowsFeature -Scope It -Exactly 2 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -Scope It -Exactly 0 + } + } +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.ps1 new file mode 100644 index 0000000..5de95f4 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.ps1 @@ -0,0 +1,131 @@ +function Set-FileBeats { +<# +.SYNOPSIS + Configures FileBeats for ORB Logging +.NOTES + Uses Alpha values +#> + + [CmdletBinding()] + Param( + # Once we have a better way to identify environments, we'll automatically set this + # For now et est mandatory + [Parameter(Mandatory = $true)] + [Alias("LogstashPort")] + [int]$Port, + + [Parameter(Mandatory = $false)] + [Alias("TargetConfigPath")] + [ValidateScript( { + try { + Test-Path $_ -PathType Leaf + } + catch { + Throw [System.Management.Automation.ItemNotFoundException] "${_} Could Not Find the Target FileBeat Configuration File. Is Filebeat Installed?" + } + })] + [string]$ConfigFilePath = (Join-Path (Get-FileBeatsPath) "filebeat.yml"), + + [Parameter(Mandatory = $false)] + [Alias("SourceConfigurationPath")] + [ValidateScript( { + try { + Test-Path $_ -PathType Container + } + catch { + Throw [System.Management.Automation.ItemNotFoundException] "${_} Could Not Find the Source Configuration Path as Specified" + } + })] + [string]$FileBeatSourceConfigurationPath = (Join-Path $PSScriptRoot "FileBeatConfiguration"), + + [Parameter(Mandatory = $false)] + [Alias("ServerGroup")] + [string]$ServerGroupName, + + [Parameter(Mandatory = $false)] + [Alias("ServerRole")] + [string]$ServerRoleName, + + [Parameter(Mandatory = $false)] + [Alias("Timezone")] + [string]$TimezoneName + ) + + $logLead = (Get-LogLeadName); + + $portHashTable = @( + + @{ Environment = "Production"; Port = 5048; }, + @{ Environment = "Staging"; Port = 5047; }, + @{ Environment = "QA"; Port = 5046; }, + @{ Environment = "Demo"; Port = 5045; } + ) + + $matchingEnvironment = ($portHashTable | Where-Object {$_.Port -eq $Port}) + if (Test-IsCollectionNullOrEmpty $matchingEnvironment) { + Write-Warning "$logLead : Could Not Find an Environment Matching Port [$Port] - Execution Cannot Continue" + return + } + + $fileBeatConfigurationSource = (Join-Path $FileBeatSourceConfigurationPath "filebeat.yml") + if (!(Test-Path $fileBeatConfigurationSource) -or !(Test-Path $fileBeatConfigurationSource)) { + Write-Warning ("$logLead : Could not find the source FileBeat Configuration File. This needs SRE review") + return + } + + Write-Output "$logLead : Configuring FileBeat to Talk to Port: [$Port] ($($matchingEnvironment.Environment))" + + # To-Do: Use Tags for this Info as Well + try { + if (!([String]::IsNullOrEmpty($ServerRoleName))) { + $role = $ServerRoleName + } + elseif ((Test-IsAppServer) -or (Test-IsMicroServer)) { + $role = "App" + } + elseif (Test-IsWebServer) { + $role = "Web" + } + else { + $role = "Unknown" + } + } + catch { + $role = "Unknown" + } + + # To-Do: Use Tags for this Info as Well + $envPodVar = [Environment]::GetEnvironmentVariable("POD", "Machine") + if (!([String]::IsNullOrEmpty($ServerGroupName))) { + $group = $ServerGroupName + } + elseif (!([String]::IsNullOrEmpty($envPodVar))) { + $group = $envPodVar + } + else { + $group = "Unknown" + } + + if (!([String]::IsNullOrEmpty($TimezoneName))) { + $timezone = $TimezoneName + } + elseif (Test-IsAws) { + $timezone = "UTC" + } + else { + $timezone = "America/Chicago" + } + + $configContent = Get-Content $fileBeatConfigurationSource + + $result = $configContent | ForEach-Object { + $_ -replace ":PORT", ":$port" ` + -replace "\{role\}", $role ` + -replace "\{group\}", $group ` + -replace "\{timezone\}", $timezone + } + + $result | Out-File $ConfigFilePath -Force +} + +Set-Alias -name Configure-FileBeats -value Set-FileBeats; diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.tests.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.tests.ps1 new file mode 100644 index 0000000..1bdbb6d --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Set-FileBeats.tests.ps1 @@ -0,0 +1,220 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-FileBeats + +Describe "Set-FileBeats" { + + # Variables for the Tests + + # Output for Validation Purposes + [array]$global:OutFile = @() + + # Legit Ports to Pass to the Tests + [array]$validPorts = @(5045, 5046, 5047, 5048) + + # Temp file to write content to + $tempPath = [System.IO.Path]::GetTempFileName() + Write-Warning ("Using temp path: $tempPath for tests") + + # Sample File Content to Update + $sampleFile = @" +#================================ Outputs ===================================== +#----------------------------- Logstash output -------------------------------- +output.logstash: + # The Logstash hosts + # This value should be modified during install to point to the logstash dns, with the port for the appropriate environment + hosts: ["orb.os-logstash.haystack.prod.alkami.net:PORT"] + # This is Custom Testing Stuff. +"@ + + #TODO: Actually fake this instead of the redirection for $fileBeatsDefaultSourceConfigPath + #TODO: Mock Get-Content with ParamFilter and use one to return $sampleFile and $templateFile(TBD) + #TODO: Abandon the filesystem entirely. Either Out-File/Write-AllLines() works or it doesn't + # Either way, it's out of scope and mocking/faking will suit our needs AND be faster + #$newTempFile = [System.IO.Path]::GetTempFileName() + #$FAKE_SourceConfigurationPath = (Get-ChildItem $newTempFile).Directory.FullName + #$templateFile(TBD) | Out-File (Join-Path $FAKE_SourceConfigurationPath "filebeat.yml") -Force + + $hereParent = Split-Path -Path $here -Parent + $fileBeatsDefaultSourceConfigPath = Join-Path -Path $hereParent -ChildPath "FileBeatConfiguration" + + Mock -ModuleName $moduleForMock -CommandName Test-IsMicroServer { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws { return $true } + + # Clean the TempPath File as Needed + function Clean-TempPath { + + if (Test-Path $tempPath) { + + Remove-Item $tempPath -Force + } + } + + Context "When a Port is Specified" { + + It "Writes a Warning if the Port Doesn't Match an Environment" { + + { (Set-FileBeats -Port 9999 3>&1) -match "Could Not Find an Environment Matching Port 9999" } | Should -BeTrue + } + + It "Uses the Specified Port in the Configuration" { + + $sampleFile | Out-File $tempPath -Force + $global:OutFile = $null + Mock -ModuleName $moduleForMock -CommandName Out-File { $global:OutFile = $_ } + + foreach ($testPort in $validPorts) { + Set-FileBeats -Port $testPort -ConfigFilePath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $global:OutFile -match "orb.os-logstash.haystack.prod.alkami.net:$testPort" | Should -BeTrue + } + } + } + + Context "When a ConfigPath is Specified" { + # Must use explicit PARAM for "SourceConfigurationPath" because ps1 path is deeper than psm1 path + It "Should Throw if the File Does Not Exist" { + { Set-FileBeats -Port $validPorts[0] -TargetConfigPath "aisjdfioajsofijasdoifasd" } | Should -Throw + } + + It "Should Throw if the Parameter is a Path" { + { Set-FileBeats -Port $validPorts[0] -TargetConfigPath "C:\Windows" } | Should -Throw + } + + It "Should Use the Specified Path" { + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "orb.os-logstash.haystack.prod.alkami.net:$($validPorts[0])" + } + } + + Context "When a Custom Source Configuration File is Provided" { + + It "Should Throw if the File Doesn't Exist" { + $nonExistantFile = [System.IO.Path]::GetTempFileName() + { Set-FileBeats -Port $validPorts[0] -SourceConfigurationPath $nonExistantFile } | Should -Throw + } + + It "Should Use the Custom Source File" { + $newTempFile = [System.IO.Path]::GetTempFileName() + $tempConfigPath = (Get-ChildItem $newTempFile).Directory.FullName + $sampleFile | Out-File (Join-Path $tempConfigPath "filebeat.yml") -Force + Set-FileBeats -Port $validPorts[0] -SourceConfigurationPath $tempConfigPath -TargetConfigPath $newTempFile + $newTempFile | Should -FileContentMatch "This is Custom Testing Stuff" + } + } + + Context "Server Role Instrumentation" { + + It "Should Use the Parameter as Provided from the Command Line" { + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -ServerRole "Foo" -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverRole: Foo" + } + + It "Should Use 'Web' if IfWebServer is True and No Parameter is Provided" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer { return $true } + Clean-TempPath + + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverRole: Web" + } + + It "Should Use 'App' if IfAppServer is True and No Parameter is Provided" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer { return $false } + Clean-TempPath + + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverRole: App" + } + + It "Should Use 'Unknown' if IfAppServer is false and Test-IsWebServer is false and No Parameter is Provided" { + Mock -ModuleName $moduleForMock -CommandName Test-IsAppServer { return $false } + Mock -ModuleName $moduleForMock -CommandName Test-IsWebServer { return $false } + Clean-TempPath + + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverRole: Unknown" + } + + It "Should Use 'Unknown' if IfAppServer / Test-IsWebServer throws and No Parameter is Provided" { + + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverRole: Unknown" + } + } + + Context "Server Group Instrumentation" { + + It "Should Use the Parameter as Provided from the Command Line" { + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -ServerGroup "Foo" -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverGroup: 'Foo'" + } + + It "Should Use the Environmental Variable POD if Present" { + # REQUIRES Admin - WILL FAIL IN VSCODE + [Environment]::SetEnvironmentVariable("POD", "FooBar", [System.EnvironmentVariableTarget]::Machine) + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + # REQUIRES Admin - WILL FAIL IN VSCODE + [Environment]::SetEnvironmentVariable("POD", $null, [System.EnvironmentVariableTarget]::Machine) + $tempPath | Should -FileContentMatch "serverGroup: 'FooBar'" + } + + it "Should use 'unknown' if no parameter as provided and the env variable is not set" { + Clean-TempPath + $samplefile | Out-File $temppath -Force + Set-FileBeats -Port $validports[0] -TargetConfigPath $temppath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "serverGroup: 'Unknown'" + } + } + + Context "Server Timezone Instrumentation" { + + It "Should Use the Parameter as Provided from the Command Line" { + + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -Timezone "Foo" -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "timezone: Foo" + } + + It "Should Be Chicago if not in AWS" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsAws { $false } + + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "timezone: America/Chicago" + } + + It "Should Be UTC if in AWS" { + + Mock -ModuleName $moduleForMock -CommandName Test-IsAws { $True } + + Clean-TempPath + $sampleFile | Out-File $tempPath -Force + Set-FileBeats -Port $validPorts[0] -TargetConfigPath $tempPath -SourceConfigurationPath $fileBeatsDefaultSourceConfigPath + $tempPath | Should -FileContentMatch "timeZone: UTC" + } + } +} + +#endregion Set-FileBeats \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Ports.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Ports.ps1 new file mode 100644 index 0000000..a8f8e24 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Ports.ps1 @@ -0,0 +1,17 @@ +function Test-Ports($ipAddresses, $port){ +<# +.SYNOPSIS + This function calls Test-NetConnection for a list of IPAddresses to test availability on a single port +#> + + $successCount = 0 + + foreach ($ipAddress in $ipAddresses){ + $tnc = Test-NetConnection -Port $port -ComputerName $ipAddress + + if ($tnc){ + $successCount++ + } + } + return $successCount +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Server.ps1 b/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Server.ps1 new file mode 100644 index 0000000..91fd18e --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/Public/Test-Server.ps1 @@ -0,0 +1,166 @@ +function Test-Server{ + <# +.SYNOPSIS + validate mininum software requirement on server +.PARAMETER dotNetMinimumVersion + The minimum .NET version allowed to be installed. Throws if the version is less than this value. Defaults to 4.7.1 +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Alkami generates this string manually, no user injection')] + [CmdletBinding()] + Param( + [string]$dotNetMinimumVersion = "4.7.1" + ) + + $logLead = (Get-LogLeadName); + + Write-Output ("$logLead : Checking Powershell Version") + if ($PSVersionTable.PSVersion.Major -lt 5 -or ($PSVersionTable.PSVersion.Major -eq 5 -and $PSVersionTable.PSVersion.Minor -eq 0)) { + Write-Warning ("$logLead : PowerShell 5.1 or Higher is Required. Detected version: {0}" -f $PSVersionTable.PSVersion) + throw ("$logLead : PowerShell 5.1 or Higher Required | https://go.microsoft.com/fwlink/?linkid=839516") + } + + Write-Output ("$logLead : Ensuring Chocolatey Version >= V0.10.11") + $currentChocolateyVersion = (Invoke-Expression "choco --version" -ErrorAction SilentlyContinue) + $version = New-Object System.Version($currentChocolateyVersion) + + if (($null -ne $version) -and ($version.Minor -ge 10 -or ($version.Minor -eq 10 -and $version.Build -ge 11))) { + + Write-Output ("$logLead : Chocolatey Version is 0.10.11 or greater. Detected version: {0}" -f $currentChocolateyVersion) + } + elseif ($null -eq $currentChocolateyVersion) + { + Write-Warning ("$logLead : Chocolatey Not Found -- Attempting to Install") + Set-ExecutionPolicy Bypass -Scope Process -Force;Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + } + else + { + Write-Warning ("$logLead : Detected Chocolatey Version {0} -- Attempting to Upgrade" -f $logVersion) + choco upgrade chocolatey -y --version 0.10.11 -s="https://chocolatey.org/api/v2/" + } + + Write-Output ("$logLead : Ensuring that a chocolatey feed source URL is not configured multiple times, and that SDK feeds are authenticated.") + $knownFeeds = @{} + $sources = choco source list -r + foreach($source in $sources) + { + $properties = $source.Split("|") + $feedName = $properties[0] + $feedUrl = $properties[1] + $isDefaultFeed = $feedName -eq "chocolatey"; + $isSdkFeed = (!$isDefaultFeed) -and ($feedUrl -notlike "*packagerepo.orb.alkamitech.com*"); + $isDisabled = if ($properties[2] -like "true") { $true; } else { $false; } + $authUser = $properties[3] + + if($isDisabled) + { + Write-Host "Skipping choco feed $feedName because it is disabled." + continue + } + + # Check that the every feed URL is only configured once. + if($knownFeeds.ContainsKey($feedUrl)) + { + throw "The feed '$feedUrl' is configured multiple times. Remove one of them!" + } + else + { + Write-Host "Discovered feed '$feedUrl'" + $knownFeeds[$feedUrl] = 1 + } + + # Check that SDK feeds are authenticated. + if($isSdkFeed -and [string]::IsNullOrWhiteSpace($authUser)) + { + throw "The SDK feed '$feedName' at source '$feedUrl' is not authenticated. Add credentials to this source!" + } + else + { + Write-Host "SDK Feed '$feedName' is authenticated." + } + } + + Write-Output ("$logLead : Checking for 7-Zip in the System Path Variable") + $paths = [Environment]::GetEnvironmentVariable("Path", "Machine") + if ($paths -notmatch '7-Zip') { + + Write-Warning ("$logLead : 7-Zip Install Directory Not Found in the System Path Variable") + throw ("$logLead : Verify 7-Zip Installed & System Env:Path Added") + } elseif (!(Invoke-Expression 7z.exe)){ + + Write-Warning ("$logLead : Could Not Invoke 7z.exe") + throw ("$logLead : 7-Zip Could Not be Called") + } + + Write-Output ("$logLead : Checking .NET Framework Version") + $dotNetVersion = Get-DotNetVersion + $dotNetMinimumKey = ([array]($Global:DotNetVersionTranslation | Where-Object {$_.FriendlyVersion -match $dotNetMinimumVersion}) | Select-Object -First 1).Key + + if ($dotNetVersion.Key -ge $dotNetMinimumKey) { + + Write-Host ("$logLead : .NET Framework {0} or higher detected, version = '{1}'" -f $dotNetMinimumVersion, $dotNetVersion.FriendlyVersion) + } + else { + Write-Verbose ("$logLead : Minimum Version Key: {0}" -f $dotNetMinimumKey) + Write-Verbose ("$logLead : Detected Version Key: {0}" -f $dotNetVersion) + $dotNetError = ("$logLead : .NET Framework {0} or Higher is Required, but Found Version '{1}'" -f $dotNetMinimumVersion, $dotNetVersion.FriendlyVersion) + Write-Warning $dotNetError + throw ($dotNetError) + } + + $requiredProducts = @( + "Microsoft Web Platform Installer 5", + "Microsoft Application Request Routing 3" + "IIS URL Rewrite Module 2" + ) + + Write-Output "$logLead : Checking for Microsoft Web Platform Installer 5+ and Required Components" + $installedProducts = Get-CIMInstance -Class Win32_Product | Select-Object -ExpandProperty Name + + $allProductsInstalled = $true + foreach ($product in $requiredProducts) { + + if ($installedProducts -match $product) + { + continue; + } + + $allProductsInstalled = $false + Write-Warning ("$logLead : Required Component <{0}> Not Found" -f $product) + } + + if (!($allProductsInstalled)) { + throw "$logLead : One or More Required Components Not Found" + } + else + { + Write-Output ("$logLead : All Expected Components Found") + } + + Write-Output "$logLead : Validating that both machine.config files are valid xml" + # This only checks that the files are valid xml. If they don't conform to MS's schema, that's a different issue that won't be caught here. + try + { + [xml]$XmlDocument = Read-MachineConfig $false + + Write-Host "$logLead : Loaded 32 bit machine.config without issues." + } + catch [System.SystemException] + { + Write-Warning "Something's broken, something's broken..." + Write-Warning $_.exception.message + throw + } + + try + { + [xml]$XmlDocument = Read-MachineConfig + + Write-Host "$logLead : Loaded 64 bit machine.config without issues." + } + catch [System.SystemException] + { + Write-Warning "Something's broken, something's broken..." + Write-Warning $_.exception.message + throw + } +} diff --git a/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServerManagement/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.nuspec b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.nuspec new file mode 100644 index 0000000..b2a46f6 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.nuspec @@ -0,0 +1,30 @@ + + + + Alkami.PowerShell.ServiceFabric + $version$ + Alkami Platform Modules - PowerShell - ServiceFabric + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.ServiceFabric + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + A set of cmdlets used to create/manage a standalone Microsoft Service Fabric cluster. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.psd1 b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.psd1 new file mode 100644 index 0000000..edce185 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.psd1 @@ -0,0 +1,19 @@ +@{ + RootModule = 'Alkami.PowerShell.ServiceFabric.psm1' + ModuleVersion = '2.6.4' + GUID = 'ed7db0d1-16fe-46a7-856c-f780b7084e0f' + Author = 'SRE' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.' + Description = 'A set of cmdlets used to create/manage a standalone Microsoft Service Fabric cluster.' + RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Choco' + FunctionsToExport = 'Clear-AlkamiServiceFabricClusterConnection','Connect-AlkamiServiceFabricCluster','Disable-AlkamiServiceFabricNode','Edit-AlkamiServiceFabricPackageLogConfig','Enable-AlkamiServiceFabricNode','Format-AlkamiEnvironmentName','Format-AlkamiEnvironmentWorkerNodeType','Format-AlkamiServiceFabricApplicationName','Get-AlkamiConnectionParameters','Get-AlkamiServiceFabricApplicationInstanceCount','Get-AlkamiServiceFabricApplications','Get-AlkamiServiceFabricNode','Get-AlkamiServiceFabricPackageServiceTypeName','Get-AlkamiServiceFabricServerCertificateName','Get-InstalledPackages','Get-PackageFromProget','Install-AlkamiServiceFabricPackages','Install-DeveloperCluster','Join-AlkamiServiceFabricCluster','New-AlkamiServiceFabricCluster','New-AlkamiServiceFabricEnvironmentNodeType','New-AlkamiServiceFabricPackage','New-AlkamiServiceFabricReliableServicePackage','Open-ServiceFabricDashboard','Publish-AlkamiServiceFabricPackages','Remove-AlkamiServiceFabricApplication','Remove-AlkamiServiceFabricApplications','Remove-AlkamiServiceFabricCluster','Remove-AlkamiServiceFabricMultipleMajorVersions','Restart-AlkamiServiceFabricApplication','Restart-AlkamiServiceFabricApplications','Set-AlkamiServiceFabricApplicationInstanceCount','Start-AlkamiServiceFabricClusterUpgrade','Test-AlkamiServiceFabricEnvironmentNodeTypeExists','Unregister-AlkamiServiceFabricApplicationTypeOlderVersions','Wait-AlkamiServiceFabricClusterHealthy','Wait-AlkamiServiceFabricNodeStatus','Wait-AlkamiServiceFabricUpgrades' + PrivateData = @{ + PSData = @{ + Tags = @('powershell', 'module', 'service', 'fabric', 'servicefabric') + ProjectUri = 'https://extranet.alkamitech.com/display/SRE/Alkami.PowerShell.ServiceFabric+Module' + IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png' + } + } + HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.PowerShell.ServiceFabric+Module' +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.pssproj b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.pssproj new file mode 100644 index 0000000..c7c5a23 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Alkami.PowerShell.ServiceFabric.pssproj @@ -0,0 +1,103 @@ + + + + Debug + 2.0 + {7ef6be1a-69ce-49ea-975c-1a085d1f1293} + Exe + MyApplication + MyApplication + Alkami.PowerShell.ServiceFabric + SRE + Alkami Technology + (c) 2017 Alkami Technology. All rights reserved. + A set of common functions and filters not typically used directly + 5151a169-c763-489e-8213-b437b7a294b1 + 0.0.0.1 + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.ServiceFabric") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + Alkami.Ops.Common + {fa9745dd-68ac-4194-9c33-acf19411d357} + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/AlkamiManifest.xml b/Modules/Alkami.PowerShell.ServiceFabric/AlkamiManifest.xml new file mode 100644 index 0000000..4b3d783 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.ServiceFabric + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..c24b8d7 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Private/VariableDeclarations.ps1 @@ -0,0 +1,4 @@ + +# Defines the names of "infrastructure microservices". +$Global:SubscriptionServiceFabricName = "Alkami.Services.Subscriptions.Host"; +$Global:InfrastructureMicroServices = @($Global:SubscriptionServiceFabricName, "Alkami.MicroServices.Broker.Host"); \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Clear-AlkamiServiceFabricClusterConnection.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Clear-AlkamiServiceFabricClusterConnection.ps1 new file mode 100644 index 0000000..6249ec9 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Clear-AlkamiServiceFabricClusterConnection.ps1 @@ -0,0 +1,19 @@ +function Clear-AlkamiServiceFabricClusterConnection { + <# + .SYNOPSIS + Disconnects the current session from a Service Fabric cluster if connected. + This is mostly useful if you need to switch between client/admin credentials on the same cluster. + #> + [CmdletBinding()] + Param() + + $loglead = (Get-LogLeadName); + + if($null -eq $Global:ClusterConnection) { + Write-Host "$loglead Not connected to a Service Fabric cluster."; + return; + } + + Write-Host "$loglead Clearing Service Fabric cluster connection."; + $Global:ClusterConnection = $null; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Connect-AlkamiServiceFabricCluster.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Connect-AlkamiServiceFabricCluster.ps1 new file mode 100644 index 0000000..c8e6d47 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Connect-AlkamiServiceFabricCluster.ps1 @@ -0,0 +1,137 @@ +function Connect-AlkamiServiceFabricCluster { + <# + .SYNOPSIS + Initiates a Service Fabric cluster connection, and stores the connection to the global scope for the session. + No action is taken if you are already connected to the cluster at the provided hostname. + + .PARAMETER hostname + The host name of any server in the Service Fabric cluster. + + .PARAMETER ServerCertificateCommonName + The common name of the server certificate to authenticate to the cluster with. + + .PARAMETER CertificateCommonName + The common name of the server OR client certificate to authenticate to the cluster with. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false, ParameterSetName = "WindowsAuth")] + [Parameter(Mandatory=$false, ParameterSetName = "CertAuth")] + [string]$Hostname = "localhost", + [Parameter(Mandatory=$true, ParameterSetName = "CertAuth")] + [string]$ServerCertificateCommonName, + [Parameter(Mandatory=$true, ParameterSetName = "CertAuth")] + [string]$CertificateCommonName + ) + + $loglead = (Get-LogLeadName); + + $endpoint = "$($hostname):19000"; + + # Check if we are already connected to this particular cluster endpoint. + Write-Verbose "$loglead Checking for existing Service Fabric connection."; + if(($null -ne $Global:ClusterConnection) -and ($Global:ClusterConnection.ConnectionEndpoint -eq $endpoint)) { + Write-Verbose "$loglead Found existing connection."; + return; + } + + # We can't connect to a cluster if there is no hostname. + if([string]::IsNullOrWhiteSpace($hostname)) { + Write-Error "$loglead No hostname provided to connect to."; + return; + } + + # If the connection certificate was not explicitly provided, look it up on the remote machine. + if([string]::IsNullOrWhiteSpace($CertificateCommonName)) { + Write-Verbose "$loglead Certificate common name was not specified. Looking up certificate to use from remote machine."; + + $clusterManifestLocation = "C:\ProgramData\SF\clusterManifest.xml"; + $clusterManifestLocation = Get-UncPath -filePath $clusterManifestLocation -ComputerName $hostname -IgnoreLocalPaths; + if(!(Test-Path $clusterManifestLocation)) { + Write-Error "$loglead : Could not find Cluster Manifest config file at '$clusterManifestLocation'. Is this server running Service Fabric?"; + return; + } + + # Read the certificate common name to connect to the cluster with. + $namespace = @{ x = "http://schemas.microsoft.com/2011/01/fabric" }; + $serverCertNode = (Select-Xml -Path $clusterManifestLocation -XPath "//x:ServerCertificate" -Namespace $namespace) | Select-Object -ExpandProperty Node -First 1; + if(!([string]::IsNullOrWhiteSpace($serverCertNode.X509FindValue))) { + $CertificateCommonName = $serverCertNode.X509FindValue; + } else { + Write-Verbose "$loglead Could not locate a certificate name in the cluster manifest. Falling back to Windows Authentication." + } + } + + # If there is no cert common name, connect with windows authentication. + if([string]::IsNullOrWhiteSpace($CertificateCommonName)) { + Write-Verbose "$loglead Opening connection to Service Fabric cluster at endpoint $endpoint with Windows Authentication"; + $connectionResult = (Connect-ServiceFabricCluster -ConnectionEndpoint $endpoint -WindowsCredential); + } else { + # Otherwise connect with a certificate. + Write-Verbose "$loglead Searching for certificate $CertificateCommonName"; + + # Search for the cert in the current user store location. + $storeLocation = "CurrentUser"; + $cert = (Find-CertificateByName -CommonName $CertificateCommonName -StoreLocation $storeLocation -StoreName My); + if($null -eq $cert) { + Write-Verbose "$loglead Could not locate certificate $CertificateCommonName in the user store location." + } + + # Search for the cert in the local machine store location. + if($null -eq $cert) { + $storeLocation = "LocalMachine"; + $cert = (Find-CertificateByName -CommonName $CertificateCommonName -StoreLocation $storeLocation -StoreName My); + if($null -eq $cert) { + Write-Verbose "$loglead Could not locate certificate $CertificateCommonName in the machine store location." + } + } + + # Return out if the certificate could not be located in either store. + if($null -eq $cert) { + Write-Error "$loglead Could not locate certificate $CertificateCommonName in the user or machine stores."; + return; + } + + Write-Verbose "$loglead Opening connection to Service Fabric cluster at endpoint $endpoint with certificate $CertificateCommonName"; + + $connectArgs = $null; + + # If the server cert common name is specified, they are connecting with an explicit server common name + client cert common name + if(!([string]::IsNullOrWhiteSpace($ServerCertificateCommonName))) { + $connectArgs = @{ + ConnectionEndpoint = $endpoint; + X509Credential = $True; + StoreLocation = "CurrentUser"; # Intentionally only search the CurrentUser store. + StoreName = "My"; + ServerCommonName = $ServerCertificateCommonName; + FindType = "FindBySubjectName"; + FindValue = $CertificateCommonName; + } + } else { + # Otherwise we are dealing with an admin cert. Connect using the same certificate for admin/client auth. + $connectArgs = @{ + ConnectionEndpoint = $endpoint; + X509Credential = $True; + StoreLocation = $storeLocation; + StoreName = "My"; + ServerCertThumbprint = $cert.Thumbprint; + FindType = 'FindByThumbprint'; + FindValue = $cert.Thumbprint; + } + } + + $connectionResult = (Connect-ServiceFabricCluster @ConnectArgs); + } + + $success = $connectionResult[0]; + if(!$success) { + Write-Error "$loglead Could not open connection to Service Fabric cluster at $endpoint"; + return; + } + + # Service fabric sets $ClusterConnection to the local scope. + # Escalate that scope to the global-scope. + $Global:ClusterConnection = $ClusterConnection; + + Write-Host "$loglead Connected to Service Fabric cluster at endpoint $endpoint"; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.Tests.ps1 new file mode 100644 index 0000000..4eeed9c --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.Tests.ps1 @@ -0,0 +1,118 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Disable-AlkamiServiceFabricNode" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Disable-AlkamiServiceFabricNode.tests' } + Mock -CommandName Connect-AlkamiServiceFabricCluster -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Disable-ServiceFabricNode -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Wait-AlkamiServiceFabricNodeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + It "Throws if Hostname is Null" { + { Disable-AlkamiServiceFabricNode -Hostname $null } | Should -Throw + } + + It "Throws if Hostname is Empty" { + { Disable-AlkamiServiceFabricNode -Hostname '' } | Should -Throw + } + + It "Throws if Intent is Not Allowable Value" { + { Disable-AlkamiServiceFabricNode -Intent 'Test' } | Should -Throw + } + + It "Throws if Sleep Interval is Zero" { + { Disable-AlkamiServiceFabricNode -SleepIntervalSeconds 0 } | Should -Throw + } + + It "Throws if Sleep Interval is Too Large" { + { Disable-AlkamiServiceFabricNode -SleepIntervalSeconds 180 } | Should -Throw + } + } + + Context "Error Handling" { + + It "Writes Error if Node is Not Present in Cluster" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test' } + } + + Disable-AlkamiServiceFabricNode -Hostname 'NotTest' + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Could not retrieve the Service Fabric node" } + } + } + + Context "Logic" { + + It "Uses Localhost Hostname By Default" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Disable-AlkamiServiceFabricNode + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + } + + It "Resolves Localhost Hostname to Computername for Node Operations" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Disable-AlkamiServiceFabricNode + + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + Assert-MockCalled -CommandName Disable-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + } + + It "Uses Hostname Parameter if Provided" { + + $testHostname = "Test" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Disable-AlkamiServiceFabricNode -Hostname $testHostname + + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + Assert-MockCalled -CommandName Disable-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.ps1 new file mode 100644 index 0000000..c88bab1 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Disable-AlkamiServiceFabricNode.ps1 @@ -0,0 +1,66 @@ +function Disable-AlkamiServiceFabricNode { + <# + .SYNOPSIS + State transitions a Service Fabric node to 'Disabled' and monitors that state transition to + completion (by default). + + .PARAMETER Hostname + [string] The host name of the server in the Service Fabric cluster to disable. If not provided, defaults to localhost. + + .PARAMETER Intent + [string] The reason the Service Fabric node is being disabled. Defaults to 'Restart'. + Refer to https://docs.microsoft.com/en-us/powershell/module/servicefabric/disable-servicefabricnode?view=azureservicefabricps + + .PARAMETER TimeoutMinutes + [byte] The length of time in minutes to monitor the node for state transition completion before declaring that an error + has occurred. Defaults to 10 minutes. + + .PARAMETER SleepIntervalSeconds + [byte] The length of time in seconds to pause between node status queries. Defaults to 15 seconds. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Hostname = 'localhost', + + [Parameter(Mandatory = $false)] + [ValidateSet('Invalid', 'Pause', 'Restart', 'RemoveData', 'RemoveNode', IgnoreCase = $false)] + [string]$Intent = 'Restart', + + [Parameter(Mandatory=$false)] + [byte]$TimeoutMinutes = 10, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 120)] + [byte]$SleepIntervalSeconds = 15 + ) + + $loglead = (Get-LogLeadName) + + Connect-AlkamiServiceFabricCluster -Hostname $Hostname + + if ( $Hostname -eq 'localhost' ) { + + $actualHostname = "$env:COMPUTERNAME" + + } else { + + $actualHostname = $Hostname + } + + # Node names are case sensitive. Resolve our hostname into a cluster node name ignoring case. + $curNode = ( Get-ServiceFabricNode | Where-Object { $_.NodeName -eq $actualHostname } | Select-Object -First 1 ) + if ( ! $curNode ) { + + Write-Error "$loglead : Could not retrieve the Service Fabric node for $actualHostname." + return + + } else { + + $nodeName = $curNode.NodeName + } + + Disable-ServiceFabricNode -NodeName $nodeName -Intent $Intent -Force + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Disabled' -Hostname $nodeName -TimeoutMinutes $TimeoutMinutes -SleepIntervalSeconds $SleepIntervalSeconds -SkipHealthCheck +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Edit-AlkamiServiceFabricPackageLogConfig.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Edit-AlkamiServiceFabricPackageLogConfig.ps1 new file mode 100644 index 0000000..96d32ba --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Edit-AlkamiServiceFabricPackageLogConfig.ps1 @@ -0,0 +1,138 @@ +function Edit-AlkamiServiceFabricPackageLogConfig { +<# +.SYNOPSIS + Edit the log config for a given package on a service fabric node, or fetches the log config + +.DESCRIPTION + Can be used to fetch a log (and optionally open it in the registered local text editor) + Can be used to bulk-overwrite the existing config in an environment with the specified file + Can be used to roll back the last change made (only one restore point in time saved) + Changes are saved before pushing a new file (for rollbacks) + Each push overwrites the previous saved changes (for rollbacks) + +.PARAMETER packageName + [string] The package being targeted + +.PARAMETER serviceFabricWorkerNode + [string] The node to be targeted. Aliases: Server, ComputerName + +.PARAMETER savePath + [string] The file target to read-to or read-from for writing to a node. Alias: ConfigPath + +.PARAMETER pull + [switch] Pull the file for this package + +.PARAMETER openInEditor + [switch] Open the pull'd file in the registered text-editor + +.PARAMETER rollback + [switch] Roll back the very last saved-changes for this package + +.PARAMETER push + [switch] Push the named file for this package + +.EXAMPLE + Edit-AlkamiServiceFabricPackageLogConfig -packageName $packageName -Server $environmentTarget -pull -openInEditor + +Opens the first package log config from the found environment into the registered text editor application + +.EXAMPLE + Edit-AlkamiServiceFabricPackageLogConfig -packageName $packageName -Server $environmentTarget -push -ConfigPath $newFileToCopyPath + +Sets the log config on each node in the environmentTarget to the file specified by the path at $newFileToCopyPath +#> + [cmdletbinding()] + param( + [Parameter(Mandatory=$true, ParameterSetName = "pull")] + [Parameter(Mandatory=$true, ParameterSetName = "push")] + [Parameter(Mandatory=$true, ParameterSetName = "rollback")] + $packageName, + [Parameter(Mandatory=$false, ParameterSetName = "pull")] + [Parameter(Mandatory=$false, ParameterSetName = "push")] + [Parameter(Mandatory=$false, ParameterSetName = "rollback")] + [Alias("Server","ComputerName")] + $serviceFabricWorkerNode = "localhost", + [Parameter(Mandatory=$false, ParameterSetName = "pull")] + [Parameter(Mandatory=$false, ParameterSetName = "push")] + [Alias("ConfigPath")] + $savePath = $PSScriptRoot, + [parameter(Mandatory=$false, ParameterSetName = "pull")] + [parameter(Mandatory=$false, ParameterSetName = "push")] + [parameter(Mandatory=$false, ParameterSetName = "rollback")] + [switch]$openInEditor, + [parameter(Mandatory=$false, ParameterSetName = "pull")] + [switch]$pull, + [parameter(Mandatory=$false, ParameterSetName = "push")] + [switch]$push, + [parameter(Mandatory=$false, ParameterSetName = "rollback")] + [switch]$rollback + ) + + $loglead = Get-LogLeadName + (Connect-AlkamiServiceFabricCluster -hostname $serviceFabricWorkerNode) | Out-Null + $environment = Format-AlkamiEnvironmentName (Get-AppSetting -AppSettingKey "Environment.Name" -ComputerName $serviceFabricWorkerNode) + + # Gets list of worker nodes for given environment + Write-Verbose "$logLead : Getting list of worker nodes" + $workerNodeType = (Format-AlkamiEnvironmentWorkerNodeType $environment) + $workerNodes = Get-ServiceFabricNode | Where-Object { $_.NodeType -eq $workerNodeType } + $workerNodeFqdns = $workerNodes | Select-Object -ExpandProperty IpAddressOrFQDN + Write-Verbose "$workerNodeFqdns" + # Find config file on any server + Write-Verbose "$logLead : Searching for log4net.config" + + # Find the application name. + $application = Get-AlkamiServiceFabricApplications -Name $packageName -EnvironmentName $environment -ComputerName $serviceFabricWorkerNode; + if($null -eq $application) { + Write-Error "$logLead : Could not find application name $packageName running in environment $environment" + return + } + + # Replace the package name with the application type name. + $packageName = $application.ServiceFabricApplicationTypeName + + # If the $SavePath is a directory, and not a specific file, auto-generate a file name. + if(Test-Path -Path $savePath -PathType Container) { + $savePath = (Join-Path $savePath "$packageName-Log4Net.config") + } + + # If we're pushing a file and the path doesn't exist, fail out. + if($push -and (!(Test-Path $savePath))) { + Write-Error "Log4net config file to push [$savePath] does not exist." + return + } + + # Search for the log4net configs on each of the worker nodes for the environment. + :workerNodeLoop foreach ($workerNode in $workerNodes) { + $nodeName = $workerNode.NodeName + $fqdn = $workerNode.IpAddressOrFQDN + $searchPath = "\\$fqdn\C$\ProgramData\SF\$nodeName\Fabric\work\Applications\$packageName*\*\log4net.config" + $FoundConfigs = Get-ChildItem -Path $searchPath | Sort-Object -property $_.Directory.name -Descending + + if ((Test-IsCollectionNullOrEmpty $FoundConfigs)) { + Write-Verbose "$logLead : Unable to find log4net config for service $packageName on $nodeName"; + } else { + foreach ($foundConfig in $FoundConfigs) { + if ($pull) { + Write-Host "$logLead : Downloading log4net.config from $foundConfig" + Copy-Item -Path $foundConfig -Destination $savePath + if ($openInEditor) { + & (Get-TextEditorPath) $savePath + } + break workerNodeLoop + } + if ($rollback) { + Write-Host "$logLead : Reverting Config on $nodeName" + Copy-Item -Path "$foundConfig.backup" -Destination $foundConfig.FullName + } + if ($push) { + Write-Verbose "$logLead : Creating Backup on $nodeName" + $backupDestinationFile = "$($foundConfig.FullName).backup" + Copy-Item -Path $foundConfig.FullName -Destination $backupDestinationFile + Write-Host "$logLead : Pushing Config to $nodeName" + Copy-Item -Path $savePath -Destination $foundConfig.FullName + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.Tests.ps1 new file mode 100644 index 0000000..16dd287 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.Tests.ps1 @@ -0,0 +1,155 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Enable-AlkamiServiceFabricNode" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Enable-AlkamiServiceFabricNode.tests' } + Mock -CommandName Connect-AlkamiServiceFabricCluster -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Enable-ServiceFabricNode -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Wait-AlkamiServiceFabricNodeStatus -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Wait-AlkamiServiceFabricClusterHealthy -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + It "Throws if Hostname is Null" { + { Enable-AlkamiServiceFabricNode -Hostname $null } | Should -Throw + } + + It "Throws if Hostname is Empty" { + { Enable-AlkamiServiceFabricNode -Hostname '' } | Should -Throw + } + + It "Throws if Sleep Interval is Zero" { + { Enable-AlkamiServiceFabricNode -SleepIntervalSeconds 0 } | Should -Throw + } + + It "Throws if Sleep Interval is Too Large" { + { Enable-AlkamiServiceFabricNode -SleepIntervalSeconds 180 } | Should -Throw + } + } + + Context "Error Handling" { + + It "Writes Error if Node is Not Present in Cluster" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test' } + } + + Enable-AlkamiServiceFabricNode -Hostname 'NotTest' + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Could not retrieve the Service Fabric node" } + } + } + + Context "Logic" { + + It "Uses Localhost Hostname By Default" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Enable-AlkamiServiceFabricNode + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + } + + It "Resolves Localhost Hostname to Computername for Node Operations" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Enable-AlkamiServiceFabricNode + + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + Assert-MockCalled -CommandName Enable-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricClusterHealthy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + } + + It "Uses Hostname Parameter if Provided" { + + $testHostname = "Test" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Enable-AlkamiServiceFabricNode -Hostname $testHostname + + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + Assert-MockCalled -CommandName Enable-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricClusterHealthy -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq $testHostname } + } + + It "Performs Health Checks By Default" { + + $testHostname = "Test" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Enable-AlkamiServiceFabricNode -Hostname $testHostname + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Enable-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricClusterHealthy -Times 1 -Exactly -Scope It + + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SkipHealthCheck -eq $false } + } + + It "Skips Health Checks if Parameter is Provided" { + + $testHostname = "Test" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname } + } + + Enable-AlkamiServiceFabricNode -Hostname $testHostname -SkipHealthCheck + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Enable-ServiceFabricNode -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricClusterHealthy -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Wait-AlkamiServiceFabricNodeStatus -Times 1 -Exactly -Scope It ` + -ParameterFilter { $SkipHealthCheck -eq $true } + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.ps1 new file mode 100644 index 0000000..aeafbcf --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Enable-AlkamiServiceFabricNode.ps1 @@ -0,0 +1,72 @@ +function Enable-AlkamiServiceFabricNode { + <# + .SYNOPSIS + State transitions a Service Fabric node to 'Up' and monitors that state transition to + completion (by default). + + .PARAMETER Hostname + [string] The host name of the server in the Service Fabric cluster to enable. If not provided, defaults to localhost. + + .PARAMETER TimeoutMinutes + [byte] The length of time in minutes to monitor the node for state transition completion before declaring that an error + has occurred. Defaults to 30 minutes. + + .PARAMETER SleepIntervalSeconds + [byte] The length of time in seconds to pause between node status queries. Defaults to 15 seconds. + + .PARAMETER SkipHealthCheck + [switch] Flag to skip node and cluster health status monitoring post-transition. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Hostname = 'localhost', + + [Parameter(Mandatory=$false)] + [byte]$TimeoutMinutes = 30, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 120)] + [byte]$SleepIntervalSeconds = 15, + + [Parameter(Mandatory=$false)] + [switch]$SkipHealthCheck + ) + + $loglead = (Get-LogLeadName) + + Connect-AlkamiServiceFabricCluster -Hostname $Hostname + + if ( $Hostname -eq 'localhost' ) { + + $actualHostname = "$env:COMPUTERNAME" + + } else { + + $actualHostname = $Hostname + } + + $curNode = Get-ServiceFabricNode | Where-Object { $_.NodeName -eq $actualHostname } | Select-Object -First 1 + if ( ! $curNode ) { + + Write-Error "$loglead : Could not retrieve the Service Fabric node for $actualHostname." + return + + } else { + + $nodeName = $curNode.NodeName + } + + Enable-ServiceFabricNode -NodeName $nodeName + + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -Hostname $nodeName -TimeoutMinutes $TimeoutMinutes -SleepIntervalSeconds $SleepIntervalSeconds -SkipHealthCheck:$SkipHealthCheck + $stopwatch.Stop() + + if ( ! $SkipHealthCheck ) { + + $timeRemaining = ( $TimeoutMinutes - $stopwatch.Elapsed.TotalMinutes ) + Wait-AlkamiServiceFabricClusterHealthy -Hostname $nodeName -TimeoutMinutes $timeRemaining -SleepIntervalSeconds $SleepIntervalSeconds + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentName.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentName.ps1 new file mode 100644 index 0000000..c5dd686 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentName.ps1 @@ -0,0 +1,29 @@ +function Format-AlkamiEnvironmentName { +<# +.SYNOPSIS + Returns the unique name of the environment with redundant pod/host information stripped out. +.PARAMETER name + The full name of the environment. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$name + ) + + # Get rid of redundant words. + $wordsToRemove = @("aws","armor","pod","production","prod","staging","stage","lane","qa","sandbox","team","development","develop","dev","regression","integration"); + foreach($word in $wordsToRemove) { + $name = ($name -replace "\b$word\b",""); + } + + # Replace dots with dashes, because ApplicationTypeName's don't like dashes. + $name = $name.Replace(".","_"); + + # Get rid of excess whitespace on the edges, and any spaces in the middle. + $name = $name.Trim(); + $name = $name.Replace(" ",""); + + return $name; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentWorkerNodeType.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentWorkerNodeType.ps1 new file mode 100644 index 0000000..838fc87 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiEnvironmentWorkerNodeType.ps1 @@ -0,0 +1,20 @@ +function Format-AlkamiEnvironmentWorkerNodeType { +<# +.SYNOPSIS + Returns the worker node type of the given environment name. + +.PARAMETER environment + The full name of the environment. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$environmentName + ) + + $shortName = (Format-AlkamiEnvironmentName -name $environmentName); + $workerName = "$($shortName)_w"; + return $workerName; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.Tests.ps1 new file mode 100644 index 0000000..988eb52 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.Tests.ps1 @@ -0,0 +1,30 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + + +Describe "Format-AlkamiServiceFabricApplicationName" { + Context "Produces Correct Values" { + + $name = "Test.Package"; + $version = "2.3.4"; + $environmentName = "QA9000"; + + It "Produces Full Name" { + $result = Format-AlkamiServiceFabricApplicationName -name $name -version $version -environmentName $environmentName; + $result | Should -be "QA9000-Test.Package/v2"; + } + It "Produces Name/Environment Name" { + $result = Format-AlkamiServiceFabricApplicationName -name $name -environmentName $environmentName; + $result | Should -be "QA9000-Test.Package"; + } + It "Produces Name/Version Name" { + $result = Format-AlkamiServiceFabricApplicationName -name $name -version $version; + $result | Should -be "Test.Package/v2"; + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.ps1 new file mode 100644 index 0000000..e8aedfd --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Format-AlkamiServiceFabricApplicationName.ps1 @@ -0,0 +1,37 @@ +function Format-AlkamiServiceFabricApplicationName { +<# +.SYNOPSIS + Returns a service name/version in the example format of "microservice.v3". + +.PARAMETER name + The package ID/name of the package. + +.PARAMETER version + The package version. +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$name, + [Parameter(Mandatory = $false)] + [Alias("e")] + [string]$environmentName, + [Parameter(Mandatory = $false)] + [Alias("v")] + [string]$version + ) + + $workingName = [string]::Empty; + if(!([string]::IsNullOrEmpty($environmentName))) { + $envName = (Format-AlkamiEnvironmentName -name $environmentName); + $workingName = "$envName-"; + } + $workingName += $name; + if(!([string]::IsNullOrEmpty($version))) { + $majorVersion = $version.Substring(0, $version.IndexOf(".")); + $workingName += "/v$majorVersion"; + } + return $workingName; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiConnectionParameters.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiConnectionParameters.ps1 new file mode 100644 index 0000000..d803ad0 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiConnectionParameters.ps1 @@ -0,0 +1,15 @@ +function Get-AlkamiConnectionParameters() + +{ +<# +.SYNOPSIS + Get Alkami connection parameters for Service Fabric +#> + + [HashTable]$connParams = @{} + + $machineName = GetMachineName -useMachineName $True + $connectionEndpoint = [string]::Concat($machineName, ":19000") + $connParams.Add("ConnectionEndpoint", $connectionEndpoint) + return $connParams +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplicationInstanceCount.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplicationInstanceCount.ps1 new file mode 100644 index 0000000..3218183 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplicationInstanceCount.ps1 @@ -0,0 +1,34 @@ +function Get-AlkamiServiceFabricApplicationInstanceCount { +<# +.SYNOPSIS + Returns the currently running instance count of a microservice. + The function fails if the service is not deployed. +.PARAMETER ServiceFabricApplicationName + The Service Fabric application name of the package to look up. +.PARAMETER ComputerName + The host name of any FAB server in the Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$ServiceFabricApplicationName, + [Parameter(Mandatory = $false)] + [Alias("server")] + [string]$ComputerName = "localhost" + ) + $loglead = (Get-LogLeadName); + Connect-AlkamiServiceFabricCluster -Hostname $ComputerName; + + # Locate the service fabric partition for the running application. + $serviceName = "$ServiceFabricApplicationName/svc"; + $partition = $null; + try { + $partition = Get-ServiceFabricPartition -ServiceName $serviceName; + } + catch { + throw "$loglead : Failed to locate Service Fabric partition for service $($ServiceFabricApplicationName): $_"; + } + + # Return the instance count of the partition. + return $partition.InstanceCount; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplications.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplications.ps1 new file mode 100644 index 0000000..70b5bd0 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricApplications.ps1 @@ -0,0 +1,105 @@ +function Get-AlkamiServiceFabricApplications { +<# +.SYNOPSIS + Returns a specific microservice package from the cluster, if it exists. +.PARAMETER Name + The name of the package to look up. +.PARAMETER Version + The optional version of the package to look for. +.PARAMETER ComputerName + The host name of any FAB server in the Service Fabric cluster. +.PARAMETER EnvironmentName + The optional environment name of the environment you are targeting. + Specifying the environment name is a performance improvement. Otherwise, it will be looked up from the remote machine. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$Name = $null, + [Parameter(Mandatory = $false)] + [string]$Version = $null, + [Parameter(Mandatory = $false)] + [Alias("server")] + [string]$ComputerName = "localhost", + [Parameter(Mandatory = $false)] + [string]$EnvironmentName = $null + ) + + $loglead = (Get-LogLeadName); + + # Connect to the cluster. + Write-Verbose "$loglead Connecting to cluster on server $ComputerName"; + Connect-AlkamiServiceFabricCluster -hostname $ComputerName | Out-Null; + + # Get the environment name so we can target packages from the specific environment, if it wasn't specified. + if([string]::IsNullOrWhiteSpace($EnvironmentName)) { + $EnvironmentName = (Get-AppSetting -appSettingKey "Environment.Name" -ComputerName $ComputerName); + } + $EnvironmentName = (Format-AlkamiEnvironmentName -name $environmentName); + + # Produce a list of packages deployed to the cluster. + Write-Verbose "$loglead Retrieving package data from Service Fabric."; + $packages = @(); + $sfPackages = (Get-ServiceFabricApplication); + foreach($package in $sfPackages) { + + # Parse the environmentName-packageName style application type name. + $packageEnvironmentName = $null; + $packageName = $null; + $typeNameSplit = $package.ApplicationTypeName.Split("-"); + if($typeNameSplit.count -gt 1) { + # Microservice Package + $packageEnvironmentName = $typeNameSplit[0]; + $packageName = $typeNameSplit[1]; + } else { + # Reliable Service + $packageEnvironmentName = $null ; + $packageName = $package.ApplicationTypeName; + } + + $packageMap = @{ + Name = $packageName + Version = $package.ApplicationTypeVersion + Environment = $packageEnvironmentName + ServiceFabricApplicationName = $package.ApplicationName + ServiceFabricApplicationTypeName = $package.ApplicationTypeName + Feed = $null + } + $packages += (New-Object -TypeName PSObject -Property $packageMap); + } + + # Filter down the packages to the appropriate environment. We don't want to accidentally target a wrong environment. + $podlessPackages = [array]($packages | Where-Object { $null -eq $_.Environment }); + $packages = [array]($packages | Where-Object { $_.Environment -eq $EnvironmentName }); + + # If a package does not have an environment, it is a podless TDE service. + # Replace the names of these packages with their actual proget package ID names by reading the name from the application manifest. + if(!(Test-IsCollectionNullOrEmpty $podlessPackages)) { + foreach($package in $podlessPackages) { + $manifestData = (Get-ServiceFabricApplicationType -ApplicationTypeName $package.ServiceFabricApplicationTypeName -ApplicationTypeVersion $package.Version); + $appParams = $manifestData.DefaultParameters; + $nugetPackageName = $appParams["NugetPackageName"].Value; + $package.Name = $nugetPackageName; + } + + # Add the podless packages into the normal list of packages. + $packages = $packages + $podlessPackages; + } + + # Filter down the package names, if it was specified. + if(!([string]::IsNullOrWhiteSpace($Name))) { + $packages = $packages | Where-Object { $_.Name -eq $Name }; + } + + # Filter down for the specific version, if it was specified. + if(!([string]::IsNullOrWhiteSpace($Version))) { + $packages = $packages | Where-Object { $_.Version -eq $Version }; + } + + # Return the list of packages. + if(Test-IsCollectionNullOrEmpty $packages) { + return $null; + } else { + return $packages; + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricNode.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricNode.ps1 new file mode 100644 index 0000000..cba600a --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricNode.ps1 @@ -0,0 +1,24 @@ +function Get-AlkamiServiceFabricNode { +<# +.SYNOPSIS + Returns the nodes of the connected Service Fabric cluster. +.PARAMETER EnvironmentName + Filters the results to the node types of a given environment name. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$EnvironmentName = $null + ) + + # Get the available nodes from Service Fabric + $results = Get-ServiceFabricNode; + + # Filter results by the nodes of a particular environment, if provided. + if(![string]::IsNullOrWhiteSpace($EnvironmentName)) { + $workerNodeType = Format-AlkamiEnvironmentWorkerNodeType -environmentName $EnvironmentName; + $results = $results | Where-Object { $_.NodeType -eq $workerNodeType} + } + + return $results; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricPackageServiceTypeName.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricPackageServiceTypeName.ps1 new file mode 100644 index 0000000..f5936b2 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricPackageServiceTypeName.ps1 @@ -0,0 +1,48 @@ +function Get-AlkamiServiceFabricPackageServiceTypeName { +<# +.SYNOPSIS + Retrieves the application type name from the svc/ServiceManifest.xml file of a Service Fabric reliable service. + Returns null if the manifest can't be found. This will not work on normal microservice packages. +.PARAMETER source + The nuget source URL of the package repository. +.PARAMETER name + The name/ID of the package. +.PARAMETER version + The version of the package. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("s")] + [string]$source, + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$name, + [Parameter(Mandatory = $true)] + [Alias("v")] + [string]$version, + [pscredential]$nugetCredential = $null + ) + + $loglead = Get-LogLeadName; + + $manifestLocation = "ApplicationManifest.xml"; + Write-Verbose "$loglead Pulling '$manifestLocation' from Proget for package $name|$version" + + try { + # Note: This manifest location -must- be located here in a valid reliable service package in order for SF to respect it. + [xml]$manifest = Get-PackageFile -feedSource $source -name $name -version $version -packagePath $manifestLocation -Credential $nugetCredential + } + catch { + Write-Error "$loglead Could not download service manifest '$manifestLocation' for package $name|$version at $source." + return $null + } + + try { + return $manifest.ApplicationManifest.ApplicationTypeName + } + catch { + Write-Error "$loglead Could not locate service type name at XML path ApplicationManifest.ApplicationTypeName"; + return $null + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricServerCertificateName.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricServerCertificateName.ps1 new file mode 100644 index 0000000..74620bc --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-AlkamiServiceFabricServerCertificateName.ps1 @@ -0,0 +1,33 @@ +function Get-AlkamiServiceFabricServerCertificateName { +<# +.SYNOPSIS + Returns the common name of the server certificate for the Service Fabric cluster running at $Hostname +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost" + ) + + $loglead = (Get-LogLeadName); + Write-Verbose "$loglead Looking up the Service Fabric server certificate from $hostname."; + + $clusterManifestLocation = "C:\ProgramData\SF\clusterManifest.xml"; + $clusterManifestLocation = Get-UncPath -filePath $clusterManifestLocation -ComputerName $hostname; + if(!(Test-Path $clusterManifestLocation)) { + Write-Error "$loglead : Could not find Cluster Manifest config file at '$clusterManifestLocation'. Is this server running Service Fabric?"; + return; + } + + # Read the certificate common name to connect to the cluster with. + $namespace = @{ x = "http://schemas.microsoft.com/2011/01/fabric" }; + $serverCertNode = (Select-Xml -Path $clusterManifestLocation -XPath "//x:ServerCertificate" -Namespace $namespace) | Select-Object -ExpandProperty Node -First 1; + if(!([string]::IsNullOrWhiteSpace($serverCertNode.X509FindValue))) { + return $serverCertNode.X509FindValue; + } else { + Write-Warning "$loglead Could not locate a certificate name in the cluster manifest."; + return $null; + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-InstalledPackages.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-InstalledPackages.ps1 new file mode 100644 index 0000000..6d223a8 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-InstalledPackages.ps1 @@ -0,0 +1,39 @@ +function Get-InstalledPackages { +<# +.SYNOPSIS + Returns a list of package names/versions deployed on the server, whether the installations + are managed by Chocolatey or Service Fabric. +.PARAMETER ComputerName + The host name of the server to get installed packages from. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("server")] + [string]$ComputerName = "localhost" + ) + + $packages = $null; + $loglead = (Get-LogLeadName); + + $isLocalFabServer = ($ComputerName -eq "localhost") -and (Test-IsServiceFabricServer); + $isRemoteFabServer = ($null -ne (Select-AlkamiFabServers $ComputerName)); + if($isLocalFabServer -or $isRemoteFabServer) { + # Fetch Service Fabric Packages + Write-Verbose "$loglead Determined $ComputerName is a Service Fabric server."; + Connect-AlkamiServiceFabricCluster -hostname $ComputerName; + $packages = (Get-AlkamiServiceFabricApplications -ComputerName $ComputerName); + } else { + # Fetch Chocolatey Packages + Write-Verbose "$loglead Determined $ComputerName is a Chocolatey managed server."; + $script = { + $result = (Get-ChocoState -local); + + # Return the results in a map so PS doesn't put needless PSComputerName/RunspaceId properties on every package. + return @{ Result = $result }; + } + $packages = (Invoke-Command -ComputerName $ComputerName -ScriptBlock $script).Result; + } + + return $packages; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-PackageFromProget.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-PackageFromProget.ps1 new file mode 100644 index 0000000..cb33d82 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Get-PackageFromProget.ps1 @@ -0,0 +1,56 @@ +function Get-PackageFromProget { +<# +.SYNOPSIS + Downloads a .nupkg from a Proget source given a package name/version. +.PARAMETER source + The nuget source URL of the package repository. +.PARAMETER name + The name/ID of the package. +.PARAMETER version + The version of the package. +.PARAMETER output + The download folder for the package. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("s")] + [string]$source = "", + [Parameter(Mandatory = $false)] + [Alias("n")] + [string]$name = "", + [Parameter(Mandatory = $false)] + [Alias("v")] + [string]$version = "", + [Parameter(Mandatory = $false)] + [Alias("o")] + [string]$output = "", + [Parameter(Mandatory = $false)] + [pscredential]$nugetCredential = $null + ) + + $loglead = Get-LogLeadName; + + Write-Host "$loglead : Downloading $name|$version from package source `"$source`""; + + # If the source has a / on the end remove it. + $lastChar = $source.Substring($source.Length-1, 1); + if(($lastChar -eq "/") -or ($lastChar -eq "\")) { + $source = $source.Substring(0, $source.Length - 1); + } + + $url = "$source/package/$name/$version"; + Write-Verbose "$loglead : Downloading package .nupkg from URL `"$url`" to `"$output`""; + try { + if(Test-Path $output) { + Write-Warning "$loglead : File `"$output`" already exists. Overwriting."; + } + $header = (Get-BasicAuthHeader -Credential $nugetCredential); + Invoke-WebRequest -Headers $header -Uri $url -OutFile $output -UseBasicParsing; + } catch { + Write-Error "$loglead : Failed to download package. `n$($_.Exception.Message)"; + return; + } + + Write-Host "$loglead : Download complete."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-AlkamiServiceFabricPackages.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-AlkamiServiceFabricPackages.ps1 new file mode 100644 index 0000000..f899d78 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-AlkamiServiceFabricPackages.ps1 @@ -0,0 +1,167 @@ +function Install-AlkamiServiceFabricPackages { +<# +.SYNOPSIS + Deploys Microservice Packages to the Service Fabric cluster. +.PARAMETER packages + Package objects for the packages to be deployed to the cluster. +.PARAMETER hostname + The host name of any server in the Service Fabric cluster. +.PARAMETER defaultLogConfigFolder + SOURCE folder for default log4net configs +.PARAMETER nugetCredential + Credential used to authenticate with Proget server +.PARAMETER HealthCheckStableDurationSec + Amount of time after 1st successful HC to require continuing successful HCs + See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-application-upgrade-parameters +.PARAMETER HealthCheckRetryTimeoutSec + Max time to keep waiting for successful HC before failing Upgrade + See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-application-upgrade-parameters +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("PackagesToPublish")] + [AllowNull()] + [object[]]$packages, + + [Parameter(Mandatory = $true)] + [Alias("s")] + [string]$hostname, + + [Parameter(Mandatory = $false)] + [string]$defaultLogConfigFolder = $null, + [Parameter(Mandatory = $false)] + [pscredential]$nugetCredential = $null, + + [Parameter(Mandatory = $false)] + [Int32]$HealthCheckStableDurationSec = 120, + + [Parameter(Mandatory = $false)] + [Int32]$HealthCheckRetryTimeoutSec = 600 + ) + + $loglead = Get-LogLeadName; + + # Arrange the subscription service and broker to the front of the packages list if they exist. + $packages = ($packages | Sort-Object -Property @{Expression={ $Global:InfrastructureMicroServices -contains $_.Name }} -Descending); + + # Remove microservice installers from the list. + $packages = $packages | Where-Object { !(Test-IsPackageInstaller -packageName $_.Name) }; + + # Write out the packages being installed. + Write-Verbose "$loglead Now installing:"; + foreach($package in $packages) { + Write-Verbose "$($package.Name) - $($package.Version)"; + } + + # Set feeds to packages if they are not already there. + Set-ChocoPackageSourceFeeds -packages $packages -hostname $hostname -Verbose; + + # Connect to SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + # Get the environment name, and clean it up. + $environmentName = Get-AppSetting -appSettingKey "Environment.Name" -ComputerName $hostname; + + # Stage all packages, which it only does fully if they aren't already staged. + Write-Host "$loglead : Staging $($packages.Count) packages."; + if ([String]::IsNullOrEmpty($defaultLogConfigFolder)) { + + Publish-AlkamiServiceFabricPackages -packages $packages -hostname $hostname -nugetCredential $NugetCredential + } else { + + Write-Host "defaultLogConfigFolder: <$defaultLogConfigFolder>" + Publish-AlkamiServiceFabricPackages -packages $packages -hostname $hostname -defaultLogConfigFolder $defaultLogConfigFolder -nugetCredential $NugetCredential + } + + # Deploy all the packages. + Write-Host "$loglead : Deploying $($packages.Count) packages."; + foreach($package in $packages) { + $packageString = "$($package.Name).$($package.Version)"; + $packageSource = $package.Feed.Source; + $isReliableService = Test-IsPackageReliableService -feedSource $packageSource -name $package.Name -version $package.Version -Credential $NugetCredential -Verbose + + # Determine application name and application type name. + $applicationName = $null; + $applicationTypeName = $null; + if($isReliableService) { + # The application type name -must- be the name of the class that implements the reliable service contract. + # Pull this name from the manifest located inside the package. + $applicationTypeName = Get-AlkamiServiceFabricPackageServiceTypeName -source $packageSource -name $package.Name -version $package.Version -nugetCredential $NugetCredential; + + # Reliable services are not tied to specific environments. + $applicationName = Format-AlkamiServiceFabricApplicationName -name $applicationTypeName -version $package.Version; + } else { + # For normal microservices the application type name is environment tied, and the application name matches the service name. + $applicationName = Format-AlkamiServiceFabricApplicationName -name $package.Name -version $package.Version -environmentName $environmentName; + $applicationTypeName = Format-AlkamiServiceFabricApplicationName -name $package.Name -environmentName $environmentName; # No version + } + $applicationPath = "fabric:/$applicationName"; + + # Figure out if we need to create a new application, or an upgrade. + $application = (Get-ServiceFabricApplication -ApplicationName $applicationPath); + + $updateAffinity = $true; + $isNewApplication = $false; + if(!($application)) { + Write-Host "$loglead : Application $($package.Name) has never been deployed! Deploying."; + $isNewApplication = $true; + New-ServiceFabricApplication -ApplicationName $applicationPath -ApplicationTypeName $applicationTypeName -ApplicationTypeVersion $package.Version + } elseif($application.ApplicationTypeVersion -ne $package.Version) { + Start-ServiceFabricApplicationUpgrade -ApplicationName $applicationPath -ApplicationTypeVersion $package.Version -Monitored -FailureAction Rollback -Force -UpgradeReplicaSetCheckTimeoutSec 15 -HealthCheckStableDurationSec $HealthCheckStableDurationSec -HealthCheckRetryTimeoutSec $HealthCheckRetryTimeoutSec + Write-Host "$loglead : Application $($package.Name) is running in the cluster on version $($application.ApplicationTypeVersion). Rolling out version $($package.Version)."; + } else { + $updateAffinity = $false; + Write-Host "$loglead : Application $packageString is already deployed on the cluster. Skipping."; + } + + # Set affinity to the subscription service. + # Only set affinity on the non-infra services. Affinity can't be set on services that run everywhere without errors. + if($updateAffinity -and ($null -eq ($Global:InfrastructureMicroServices | Where-Object { $_ -eq $package.name; }))) { + Write-Verbose "$loglead : Setting Affinity to the Subscription Service"; + + # Only query for the subscription service once. This only matters if the subscription service is not yet deployed. + if($null -eq $subscriptionService) { + # Query for the subscription service app in SF. + # Grab all apps and wildcard search because we don't know what major version might be running, and SF doesn't do partial searches. + Write-Verbose "$loglead : Searching for subscription service application name in the cluster."; + $subscriptionAppName = (Format-AlkamiServiceFabricApplicationName -name $Global:SubscriptionServiceFabricName -environmentName $environmentName); + $subscriptionAppNameUri = "fabric:/$($subscriptionAppName)*"; + $subscriptionApplication = (Get-ServiceFabricApplication | Where-Object { $_.ApplicationName -like $subscriptionAppNameUri} | Select-Object -First 1); + if($null -ne $subscriptionApplication) { + $subscriptionService = Get-ServiceFabricService -ApplicationName $subscriptionApplication.ApplicationName; + } + if($null -eq $subscriptionService) { + Write-Verbose "$loglead : Could not find subscription service to set affinity to."; + } else { + Write-Verbose "$loglead : Found $($subscriptionService.ServiceName)"; + } + } + if($null -ne $subscriptionService) { + $affinity = "NonAlignedAffinity"; + $affinityRules = @("$($subscriptionService.ServiceName),$affinity"); + + # Set affinity for every service in the application that was just deployed. + $services = Get-ServiceFabricService -ApplicationName $applicationPath; + foreach($service in $services) { + Update-ServiceFabricService -Stateless -ServiceName $service.ServiceName -Correlation $affinityRules -Force; + } + Write-Verbose "$loglead : Affinity update complete."; + } + } + + # If we deployed a new major version, instead of an upgrade of an existing major version, there will be multiple applications running. + # Remove every major version of the application that is not the version we just deployed. + if($isNewApplication) { + $runningApplications = Get-AlkamiServiceFabricApplications -ComputerName $hostname -Name $package.Name; + $oldApplications = $runningApplications | Where-Object { $_.Version -ne $package.Version }; + if(!(Test-IsCollectionNullOrEmpty $oldApplications)) { + Write-Host "$loglead : New major version is deployed. Removing other major versions."; + foreach($application in $oldApplications) { + Write-Host "$loglead : Removing $($application.Name) $($application.Version) from cluster."; + Remove-ServiceFabricApplication -ApplicationName $($application.ServiceFabricApplicationName) -Force; + } + } + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-DeveloperCluster.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-DeveloperCluster.ps1 new file mode 100644 index 0000000..b60f013 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Install-DeveloperCluster.ps1 @@ -0,0 +1,92 @@ +function Install-DeveloperCluster { +<# +.SYNOPSIS + Installs Developer Service Fabric Cluster. +.PARAMETER dataRoot + Local developer cluster data directory SF executes from +.PARAMETER logRoot + Local developer cluster log directory SF logs to +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", '', Justification="We know if we are calling the right parameters")] + param( + $dataRoot = "C:\SFDevCluster\Data", + $logRoot = "C:\SFDevCluster\Log" + ) + + $clusterConfigFilePath = (Join-Path $PSScriptRoot "ServiceFabricConfigTemplates\AlkamiDevClusterConfig.json"); + # Import the cluster setup utility module + If(!(Test-RegistryKey "HKLM:\Software\Microsoft\Service Fabric SDK")) { + Throw "Service Fabric SDK is not installed" + } + + $sdkInstallPath = (Get-ItemProperty 'HKLM:\Software\Microsoft\Service Fabric SDK').FabricSDKScriptsPath + $modulePath = Join-Path -Path $sdkInstallPath -ChildPath "ClusterSetupUtilities.psm1" + Import-Module $modulePath + Write-Host "Checking to see if a local cluster is installed" + if (IsLocalClusterSetup) { + Write-Host "Cleaning the existing cluster..." + CleanExistingCluster + } + # Stop SharedAccess from interfering with service fabric startup + Write-Host "Prepping Machine Services..." + $internetConnectionSharingService = Get-Service SharedAccess + if ($internetConnectionSharingService -and $internetConnectionSharingService.Status -eq "Running") { + $internetConnectionSharingService | Set-Service -StartupType Disabled + $internetConnectionSharingService.Stop(); + } + # Start RemoteRegistry as its seemingly needed for service fabric to start up + $remoteRegistryService = Get-Service RemoteRegistry + if ($remoteRegistryService -and $remoteRegistryService.Status -ne "Running") { + $remoteRegistryService | Set-Service -StartupType Automatic + $remoteRegistryService.Start(); + } + + Add-HostsFileContent -contentToAdd "127.0.0.1 microservices.dev.alkamitech.com" + if(!(Test-IsAdmin)) { + Throw "Not running as administrator. You need to run PowerShell with administrator privileges to setup the local cluster." + } + + $clusterRoots = SetupDataAndLogRoot -clusterDataRoot $dataRoot -clusterLogRoot $logRoot -jsonFileTemplate $clusterConfigFilePath -isAuto $True + $manifestFileTemplate = ConstructManifestFileTemplate -jsonTemplate $clusterConfigFilePath + $clusterDataRoot = $clusterRoots[0] + $clusterLogRoot = $clusterRoots[1] + Write-Host "Setting up image store..." + $imageStoreConnectionString = SetupImageStore -clusterDataRoot $clusterDataRoot -useImageStoreService $False + Write-Host "Prepping cluster manifest..." + $manifestFile = PrepareClusterManifest $manifestFileTemplate $imageStoreConnectionString "localhost" $False $False + + if($clusterRoots[0] -eq $False) { + Throw "Failed to generate ServiceFabric Cluster" + } + + PerformServiceOperationWithWaitforStatus "FabricHostSvc" "Stop-Service" "Stopped" 10 5 + try { + New-ServiceFabricNodeConfiguration -ClusterManifest "$manifestFile" -FabricDataRoot "$clusterDataRoot" -FabricLogRoot "$clusterLogRoot" -RunFabricHostServiceAsManual + } + catch { + Throw "Could not create Node configuration for '$manifestFile'" + } + + Set-ItemProperty 'HKLM:\Software\Microsoft\Service Fabric SDK' -Name LocalClusterNodeCount -Value 1 + Set-ItemProperty 'HKLM:\Software\Microsoft\Service Fabric SDK' -Name IsMeshCluster -Value "false" + # Save Connection Parameters in %AppData% for REST based powershell to consume. + Write-Host "Saving connection parameters..." + SaveConnectionParameters -dataRoot $clusterDataRoot -isSecure $False -useMachineName $False + StartLocalCluster + $connParams = Get-AlkamiConnectionParameters + TryConnectToCluster -connParams $connParams -waitTime 240 + CheckNamingServiceReady -connParams $connParams -waitTime 120 + $outputString = @" + Local Service Fabric Cluster created successfully. + + ================================================= + ## To connect using Powershell, open an a new powershell window and connect using 'Connect-ServiceFabricCluster' command (without any arguments)." + + ## To connect using Service Fabric Explorer, run ServiceFabricExplorer and connect using 'Local/OneBox Cluster'." + + ## To manage using Service Fabric Local Cluster Manager (system tray app), run ServiceFabricLocalClusterManager.exe" + ================================================= +"@ + + Write-Host $outputString -ForegroundColor Green +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Join-AlkamiServiceFabricCluster.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Join-AlkamiServiceFabricCluster.ps1 new file mode 100644 index 0000000..607b3d5 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Join-AlkamiServiceFabricCluster.ps1 @@ -0,0 +1,137 @@ +function Join-AlkamiServiceFabricCluster { +<# +.SYNOPSIS + Adds the executing server into an existing ServiceFabric cluster. Returns $true on success. + +.PARAMETER servers + The server hostname(s) of an existing cluster. Only one is required. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $true)] + [string[]]$Servers + ) + + $loglead = (Get-LogLeadName); + + # Make sure we're not installing this on a web server. + if(Test-IsWebServer) { + Write-Error "$loglead : Cannot install Service Fabric on a Web server!"; + return $false; + } + + if(($null -eq $servers) -or ($servers.count -eq 0)) + { + Write-Error "$loglead No SF cluster servers passed in to join. Exiting."; + return; + } + + $endpointPort = 19000; + + Write-Host "$loglead : Scanning for an existing Service Fabric cluster to join."; + $localIP = (Get-IpAddress); + $existingClusterNodeIp = $null; + foreach($server in $servers) { + if($server -eq $localIP) { + continue; + } + + Write-Host "$loglead : Testing endpoint $($server):$endpointPort for a Service Fabric cluster."; + $testEndpoint = (Test-NetConnection -ComputerName $server -Port $endpointPort); + + if($testEndpoint.TcpTestSucceeded) { + $existingClusterNodeIp = $server; + break; + } + } + + if($null -eq $existingClusterNodeIp) { + Write-Host "$loglead : Could not find a running service fabric cluster to join server to."; + return $false; + } + + # Connect to an existing cluster if one exists. + Write-Host "$loglead : Downloading Service Fabric installer/runtime files via Chocolatey." + choco upgrade Alkami.DevOps.ServiceFabric -yr; + + $chocoInstallPath = Get-ChocolateyInstallPath + $fabricBasePath = Join-Path $chocoInstallPath "lib\Alkami.DevOps.ServiceFabric\files" + if(!(Test-Path $fabricBasePath)) { + Write-Error "$loglead : Service Fabric was not downloaded correctly. Check the Chocolatey logs."; + return $false; + } + + # Make sure that the offline installation .cab was downloaded successfully. + $runtime = Get-ChildItem -Path $fabricBasePath -Filter "*.cab" | select-object -First 1; + if(($null -eq $runtime) -or (!(Test-Path $runtime.FullName))) { + Write-Error "$loglead : The Service Fabric offline installation .cab was not downloaded correctly. Check the chocolatey logs."; + return $false; + } + $runtimePath = $runtime.FullName; + + # Determine the name of the SF node and its fqdn for windows authentication. + $nodeName = $env:COMPUTERNAME; + $fqdn = (Get-FullyQualifiedServerName); + + # Determine endpoint of the existing cluster endpoint we're joining. + $endpoint = "{0}:$endpointPort" -f $existingClusterNodeIp; + + # Determine fault/upgrade domain. + $faultDomain = $null; + $hashcode = [Math]::Abs($fqdn.GetHashCode()); + + # Use the hostname as the AZ to get around overtly strict rules around microservice placements in SF + $az = $env:COMPUTERNAME; + + # Set the fault domain to the availability zone in AWS. + $faultDomain = "fd:/{0}/r0" -f $az; + $upgradeDomain = $hashcode; + + $environmentName = (Get-AppSetting -appSettingKey "Environment.Name"); + $workerName = (Format-AlkamiEnvironmentWorkerNodeType $environmentName); + + # TODO: Fix this to remove a manual setup step. + # The issue is that this relies on SF module functions to work. + # However, the SF module is installed with the AddNode.ps1 script below. + # New-AlkamiServiceFabricEnvironmentNodeType -environmentName $environmentName; + + # Look up the server certificate name from the seed node. + $clusterManifestLocation = "C:\ProgramData\SF\clusterManifest.xml"; + $clusterManifestLocation = Get-UncPath -filePath $clusterManifestLocation -ComputerName ($Servers[0]); + if(!(Test-Path $clusterManifestLocation)) { + Write-Error "$loglead : Could not find Cluster Manifest config file at '$clusterManifestLocation'."; + return; + } + + # Read the certificate common name to connect to the cluster with. + $ServerCertificateCommonName = $null; + $namespace = @{ x = "http://schemas.microsoft.com/2011/01/fabric" }; + $serverCertNode = (Select-Xml -Path $clusterManifestLocation -XPath "//x:ServerCertificate" -Namespace $namespace) | Select-Object -ExpandProperty Node -First 1; + if(!([string]::IsNullOrWhiteSpace($serverCertNode.X509FindValue))) { + $ServerCertificateCommonName = $serverCertNode.X509FindValue; + } else { + Write-Error "$loglead Could not locate a certificate common name in the cluster manifest. Exiting.." + return; + } + + # Look up the certificate to connect to the cluster with. + $serverCert = Find-CertificateByName -CommonName $ServerCertificateCommonName -StoreLocation "LocalMachine" -StoreName "My"; + if($null -eq $serverCert) { + Write-Host "$loglead Could not locate certificate $ServerCertificateCommonName. Make sure it is loaded to the local machine store."; + } + $serverCertThumbprint = $serverCert.Thumbprint; + + Write-Host "$loglead : Connecting to existing Service Fabric cluster on node $existingClusterNodeIp"; + $installerPath = Join-Path $fabricBasePath "AddNode.ps1"; + & $installerPath -NodeName $nodeName -NodeType $workerName -NodeIPAddressorFQDN $fqdn -ExistingClientConnectionEndpoint $endpoint -UpgradeDomain $upgradeDomain -FaultDomain $faultDomain -FabricRuntimePackagePath $runtimePath -AcceptEULA -X509Credential -ServerCertThumbprint $serverCertThumbprint -StoreLocation "LocalMachine" -StoreName "My" -FindValueThumbprint $serverCertThumbprint -Verbose; + + # See if the SF port is listening as a test. The SF AddNode script doesn't actually fail. + Start-Sleep -Seconds 5; + if ($null -eq (Get-NetTCPConnection | Where-Object {($_.LocalPort -eq $endpointPort) -and ($_.State -eq "Listen")})) { + Write-Error "$loglead : Server did not successfully join cluster. Read logs."; + return $false; + } + + return $true; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricCluster.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricCluster.ps1 new file mode 100644 index 0000000..cd9382b --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricCluster.ps1 @@ -0,0 +1,215 @@ +function New-AlkamiServiceFabricCluster { + <# + .SYNOPSIS + Creates a service fabric cluster with the set of servers and a security group prefix. + Security group prefix is 'Stage' for Staging, 'DEV' for QA + .PARAMETER servers + The servers to join together into a service fabric cluster. + .PARAMETER securityGroupPrefix + The prefix of the "X - GMSA" security group that the servers are in. + .PARAMETER serverCertificateCommonName + The common name of the admin certificate used to administer the cluster, and secure the service fabric dashboard. + .PARAMETER clientCertificateCommonName + The common name of the client certificate used to gain read-only access to the cluster, and service fabric dashboard. + #> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string[]]$Servers, + [Parameter(Mandatory = $true)] + [string]$SecurityGroupPrefix, + [Parameter(Mandatory = $true)] + [string]$ServerCertificateCommonName, + [Parameter(Mandatory = $true)] + [string]$ClientCertificateCommonName + ) + + $loglead = (Get-LogLeadName); + Write-Host "$loglead : Initializing Service Fabric cluster."; + + # Determine if this cluster is being created for a non-production environment. + $devEnvironmentTypes = @( + "Development", + "QA", + "TeamQA", + "Sandbox" + ) + $environmentType = (Get-AppSetting -appSettingKey "Environment.Type" -ComputerName $servers[0]); + $isDevEnvironment = $null -ne ($devEnvironmentTypes | Where-Object { $_ -eq $environmentType; }); + + $nodes = @(); + foreach($server in $servers) { + # If the server name passed in is a FQDN, strip off everything after the "." for convenience of reading later. + $nodeName = $server; + $dotIndex = $nodeName.IndexOf("."); + if($dotIndex -ge 0) { + $nodeName = $nodeName.Substring(0, $dotIndex); + } + + $hashcode = [Math]::Abs($server.GetHashCode()); + $availabilityZone = Get-AvailabilityZone -ComputerName $server; + if($null -eq $availabilityZone) { + $availabilityZone = $hashcode; + } + $faultDomain = "fd:/{0}/r0" -f $availabilityZone; + + $nodes += @{ + NodeName = $nodeName; + IPAddress = $server; + FaultDomain = $faultDomain + UpgradeDomain = $hashcode; + } + } + + # If it's a development environment and there are less than 3 AZ's, tack unique numbers onto the end of the AZ's. + # This is to trick Service Fabric into believing that it has 3 AZ's so that the cluster bootstraps successfully. + if($isDevEnvironment) + { + $uniqueFaultDomains = $nodes | Foreach-Object { $_["FaultDomain"] } | Select-Object -Unique; + if($uniqueFaultDomains.Count -lt 3) + { + $counter = 1; + foreach($node in $nodes) + { + $node["FaultDomain"] = $node["FaultDomain"] + "-$counter"; + $counter++; + } + } + } + + $podName = (Get-AppSetting -appSettingKey "Environment.Name" -ComputerName $servers[0]) + if($null -eq $podName) { + throw "$loglead : Environment.Name machine config app setting value must be specified!"; + } + $podName = $podName.Replace(" ","-"); + $clusterName = "$podName-Fabric"; + Write-Host "$loglead : Creating Cluster $clusterName`n"; + + Write-Host "$loglead : Downloading Service Fabric installer/runtime files via Chocolatey." + choco upgrade Alkami.DevOps.ServiceFabric -y --no-progress; + + $chocoInstallPath = Get-ChocolateyInstallPath + $fabricBasePath = Join-Path $chocoInstallPath "lib\Alkami.DevOps.ServiceFabric\files" + if(!(Test-Path $fabricBasePath)) { + throw "$loglead : Service Fabric was not downloaded correctly. Check the Chocolatey logs."; + } + + # Make sure that the offline installation .cab was downloaded. + $runtime = Get-ChildItem -Path $fabricBasePath -Filter "*.cab" | Select-Object -First 1 + if(($null -eq $runtime) -or (!(Test-Path $runtime.FullName))) { + throw "$loglead : The Service Fabric offline installation .cab was not downloaded correctly. Check the chocolatey logs." + } + $runtimePath = $runtime.FullName; + + $templateFileName = "ClusterConfig.json"; + $fabricConfigTemplatePath = (Join-Path $PSScriptRoot "ServiceFabricConfigTemplates/$templateFileName"); + $clusterConfigTemplatePath = (Join-Path $fabricBasePath $templateFileName); + Copy-Item -Path $fabricConfigTemplatePath -Destination $clusterConfigTemplatePath -Force + + if(!(Test-Path $clusterConfigTemplatePath)) { + throw "$loglead : Cannot locate cluster configuration template file."; + } + + Write-Host "`n$loglead : Building service fabric cluster definition."; + $config = ((Get-Content -Path $clusterConfigTemplatePath) | ConvertFrom-Json); + + # Define cluster name. + $config.name = $clusterName; + + # Define nodes in the cluster. + $tempNode = $config.nodes[0].PsObject.Copy(); + $config.nodes = @(); + foreach($serverNode in $nodes) { + $xmlNode = $tempNode.PsObject.Copy(); + + $xmlNode.nodeName = $serverNode.NodeName; + $xmlNode.ipAddress = $serverNode.IPAddress; + $xmlNode.faultDomain = $serverNode.FaultDomain; + $xmlNode.upgradeDomain = $serverNode.UpgradeDomain; + + $config.nodes += $xmlNode; + } + + # Configure gmsa cluster security. + Write-Host "$loglead : Now configuring GMSA account security for the cluster."; + $gmsaGroup = "$securityGroupPrefix - gMSA"; + + Write-Host "$loglead : Configuring security within the `"$gmsaGroup`" group."; + $config.properties.security.WindowsIdentities.ClusterIdentity = $gmsaGroup; + + Write-Host "$loglead : Configuring certificates." + $config.properties.security.CertificateInformation.ServerCertificateCommonNames.CommonNames[0].CertificateCommonName = $serverCertificateCommonName; + + # Find certificates for the cluster. + $adminCert = Find-CertificateByName -CommonName $serverCertificateCommonName -StoreLocation LocalMachine -StoreName "My"; + if($null -eq $adminCert) { + throw "$loglead : Could not locate server certificate $serverCertificateCommonName. Please make sure that it exists."; + } + $clientCert = Find-CertificateByName -CommonName $clientCertificateCommonName -StoreLocation LocalMachine -StoreName "My"; + if($null -eq $clientCert) { + throw "$loglead : Could not locate server certificate $serverCertificateCommonName. Please make sure that it exists."; + } + + Write-Host "$loglead : Fetching issuing certificate thumbprints for admin/client certificates." + + # Find the thumbprint of the issuing certificate for the admin/client certificates. + $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain; + + # Find the issuing thumbprint of the admin cert. + $chain.Build($adminCert); + $adminIssuerThumbprint = $chain.ChainElements[1].Certificate.Thumbprint; + $chain.Reset(); + + # Find the issuing thumbprint of the client cert. + $chain.Build($clientCert); + $clientIssuerThumbprint = $chain.ChainElements[1].Certificate.Thumbprint; + $chain.Reset(); + + # Configure admin cert. + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[0].CertificateCommonName = $serverCertificateCommonName; + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[0].CertificateIssuerThumbprint = $adminIssuerThumbprint; + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[0].IsAdmin = $true; + + # Configure client cert. + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[1].CertificateCommonName = $clientCertificateCommonName; + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[1].CertificateIssuerThumbprint = $clientIssuerThumbprint; + $config.properties.security.CertificateInformation.ClientCertificateCommonNames[1].IsAdmin = $false; + + $clusterConfigLocation = "$fabricBasePath\ClusterConfig.json"; + Write-Host "$loglead : Saving Service Fabric cluster definition to $clusterConfigLocation"; + Set-Content -Path $clusterConfigLocation -Value ($config | ConvertTo-Json -Depth 8); + + Write-Host "$loglead : Starting Remote Registry service on all machines."; + $script = { + param($loglead) + + $server = $env:COMPUTERNAME; + $serviceName = "RemoteRegistry"; + $service = (Get-Service $serviceName); + + if($null -eq $service) { + return; + } + + $enabled = ($service.StartType -ne "Disabled"); + if(!$enabled) { + Write-Host "$loglead : Setting $serviceName to manual on $server"; + Set-Service $serviceName -StartupType Manual; + } + + $running = $service.Status -eq "Running" + if(!$running) { + Write-Host "$loglead : Starting $serviceName on $server"; + Start-Service $serviceName; + } + } + Invoke-Command -ComputerName $servers -ScriptBlock $script -ArgumentList $loglead; + + $installerPath = (Join-Path $fabricBasePath "CreateServiceFabricCluster.ps1"); + + Write-Host "$loglead : Creating cluster."; + Write-Host "$loglead : InstallerScriptPath: $installerPath"; + Write-Host "$loglead : ClusterConfigPath: $clusterConfigLocation"; + Write-Host "$loglead : FabricRuntimePath: $runtimePath"; + & $installerPath -ClusterConfigFilePath $clusterConfigLocation -FabricRuntimePackagePath $runtimePath -AcceptEULA -Verbose; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricEnvironmentNodeType.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricEnvironmentNodeType.ps1 new file mode 100644 index 0000000..5cfd7da --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricEnvironmentNodeType.ps1 @@ -0,0 +1,102 @@ +function New-AlkamiServiceFabricEnvironmentNodeType { +<# +.SYNOPSIS + Creates a Service Fabric node type for a given environment name. +.PARAMETER environmentName + The name of an orb environment. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +.PARAMETER timeoutMinutes + The amount of time until the function fails. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$environmentName, + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + [Parameter(Mandatory = $false)] + [int]$timeoutMinutes = 60 + ) + + $loglead = (Get-LogLeadName); + + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + $nodeTypeExists = (Test-AlkamiServiceFabricEnvironmentNodeTypeExists -environmentName $environmentName); + if($nodeTypeExists) { + Write-Host "$loglead The worker node type for environment $environmentName already exists. Returning."; + return; + } + + # Query the cluster node configuration, and pull out the node type names. + $clusterConfigJson = Get-ServiceFabricClusterConfiguration | ConvertFrom-Json; + + # Increment the cluster configuration version. + $version = $clusterConfigJson.ClusterConfigurationVersion; + $minorVersionDotIndex = $version.LastIndexOf('.'); + $minorVersion = $version.Substring($minorVersionDotIndex + 1); + $minorVersion = [int]$minorVersion + 1; + $version = $version.Substring(0, $minorVersionDotIndex) + "." + $minorVersion; + $clusterConfigJson.ClusterConfigurationVersion = $version; + + # Remove the "CertificateInformation" section because fabric complains when it is posted back. + # This is OK because we use windows auth to secure the cluster. + if(($clusterConfigJson.Properties.Security.CertificateInformation | Get-Member -MemberType NoteProperty).Count -eq 1) + { + $clusterConfigJson.Properties.Security = ($clusterConfigJson.Properties.Security | Select-Object -Property * -ExcludeProperty "CertificateInformation"); + } + + # Determine the name of the node type. + $workerName = (Format-AlkamiEnvironmentWorkerNodeType $environmentName); + + # Copy one of the existing worker nodes, update the name, and set IsPrimary to false (for not a seed-node) + $newNode = $clusterConfigJson.Properties.NodeTypes[0].PsObject.Copy(); + $newNode.Name = $workerName; + $newNode.IsPrimary = $false; + + # Add the new worker node type. + $clusterConfigJson.Properties.NodeTypes += $newNode; + + # Write the new cluster config out to a file, because the SF function won't accept the raw json. + try { + $newManifest = ($clusterConfigJson | ConvertTo-Json -Depth 20); + $fileLocation = "./newClusterConfig.json"; + Set-Content -Path $fileLocation -Value $newManifest; + + Write-Host "$loglead Initiating cluster upgrade. Now waiting for configuration rollout to complete. Timeout is $($timeoutMinutes)m."; + $timer = [System.Diagnostics.Stopwatch]::StartNew(); + Start-ServiceFabricClusterConfigurationUpgrade -ClusterConfigPath $fileLocation -TimeoutSec ($timeoutMinutes * 60); + + Start-Sleep -s 5; + $status = (Get-ServiceFabricClusterConfigurationUpgradeStatus).UpgradeState; + + $upgradeDone = $false; + while(!$upgradeDone) { + $newStatus = (Get-ServiceFabricClusterConfigurationUpgradeStatus).UpgradeState; + if($status -ne $newStatus) { + $status = $newStatus + Write-Host "$loglead Cluster upgrade status changed to $newStatus;" + } + + if($status -eq "RollingForwardCompleted") { + Write-Host "$loglead Successfully added nodetype '$workerName'"; + $upgradeDone = $true; + } elseif (($status -eq "RollingBackCompleted") -or ($status -eq "Failed")) { + $upgradeDone = $true; + throw "$loglead Failed to add nodetype '$workerName'. Configuration was rolled back."; + } + + if((!$upgradeDone) -and ($timer.Elapsed.TotalMinutes -gt $timeoutMinutes)) { + throw "$loglead Timeout of $($timer.Elapsed.TotalMinutes)m elapsed. Check the dashboard to see if the upgrade is still running."; + } + Start-Sleep -Seconds 15; + } + Write-Host "$loglead Finished upgrade in $($timer.Elapsed.TotalMinutes) minutes."; + + } finally { + if(Test-Path $fileLocation) + { + Remove-Item -Path $fileLocation -Force; + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricPackage.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricPackage.ps1 new file mode 100644 index 0000000..5238031 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricPackage.ps1 @@ -0,0 +1,281 @@ +function New-AlkamiServiceFabricPackage { + <# +.SYNOPSIS + Turns a Chocolatey/Nuget package into the file/folder input that Service Fabric requires for deployments. + Note: The output folder is -not- automatically cleaned up! Delete it so avoid piling up memory. +.PARAMETER source + The nuget URL of the package repository. +.PARAMETER name + The name/ID of the package to be created. +.PARAMETER version + The version of the package to be created. +.PARAMETER userMicro + The name of the non-database microservice user gmsa account. +.PARAMETER userDbms + The name of the database microservice user gmsa account. +.PARAMETER defaultInstanceCount + The default number of microservice instances that will be deployed. +.PARAMETER outputFolder + The download/output directory of the package. +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("s")] + [string]$source, + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$name, + [Parameter(Mandatory = $true)] + [Alias("v")] + [string]$version, + [Parameter(Mandatory = $true)] + [string]$environmentName, + [Parameter(Mandatory = $true)] + [string]$environmentType, + [Parameter(Mandatory = $true)] + [string]$userMicro, + [Parameter(Mandatory = $true)] + [string]$userDbms, + [Parameter(Mandatory = $true)] + [int]$defaultInstanceCount, + [Parameter(Mandatory = $true)] + [Alias("o")] + [string]$outputFolder, + [Parameter(Mandatory = $false)] + [string]$defaultLogConfigFolder = $null, + [Parameter(Mandatory = $false)] + [switch]$force, + [Parameter(Mandatory = $false)] + [pscredential]$nugetCredential = $null + ) + + $loglead = Get-LogLeadName; + + $packageString = "$name|$version"; + Write-Host "$loglead : Creating Service Fabric package for $packageString"; + + # Download Alkami package. + if (Test-Path $outputFolder) { + if (!$force) { + throw "$loglead : Directory `"$outputFolder`" already exists. If you intend to overwrite it use -force."; + } else { + Write-Host "$loglead : Recreating directory $outputFolder"; + Remove-Item -Path $outputFolder -Recurse -Force; + New-Item -Path $outputFolder -ItemType Directory | Out-Null; + } + } else { + Write-Host "$loglead : Creating directory $outputFolder"; + New-Item -Path $outputFolder -ItemType Directory | Out-Null; + } + + $tempPackagePath = (Join-Path $outputFolder "nugetDownload.zip"); + $fabricConfigTemplatesPath = (Join-Path $PSScriptRoot "ServiceFabricConfigTemplates"); + + try { + # Download specified package from nuget feed. + Write-Verbose "$loglead : Downloading package $packageString from proget to `"$tempPackagePath.`""; + Get-PackageFromProget -source $source -name $name -version $version -output $tempPackagePath -nugetCredential $nugetCredential; + + # Get a version of the microservice name with the "v3" style version name on the end. + $environmentShortName = Format-AlkamiEnvironmentName -name $environmentName; + $applicationTypeName = (Format-AlkamiServiceFabricApplicationName -name $name -environmentName $environmentName); + $serviceName = "svc"; + # Note: The service name "svc" is for short service fabric pathing. This is not actually the name of the service. + # The full name of the service will look like this: "Alkami.Widget.Sample/v1/svc/" + + # Create Service Fabric directory structure. + Write-Verbose "$loglead : Creating SF package directory structure in `"$outputFolder`""; + $subfolders = @( + $serviceName, + "$serviceName\Code", + "$serviceName\Config" + ) + for ($i = 0; $i -lt $subfolders.Count; $i++) { + $path = (Join-Path $outputFolder $subfolders[$i]); + if (!(Test-Path $path)) { + New-Item -ItemType directory -Path $path -Force | Out-Null; + } + Write-Verbose "$loglead : Created $path"; + $subfolders[$i] = $path; + } + $serviceRoot = $subfolders[0]; + $codeRoot = $subfolders[1]; + $configRoot = $subfolders[2]; + + # Unzip the .nupkg to where Service Fabric expects files to be. + Write-Verbose "$loglead : Unzipping $packageString from `"$tempPackagePath`" to `"$codeRoot`""; + $szPath = Get-7ZipPath + & $szPath x "$tempPackagePath" -aoa -o"$codeRoot" "tools/*" -r | Out-Null + & $szPath e "$tempPackagePath" -aoa -o"$codeRoot" "*.nuspec" -r | Out-Null + + # Copy everything in the tools folder up a directory and remove it. + $toolsFolder = "$codeRoot/tools"; + $items = Get-ChildItem -Path $toolsFolder; + foreach ($item in $items) { + Move-Item -Path $item.FullName -Destination $codeRoot; + } + Remove-Item $toolsFolder -Force; + + # Clean up nuget .nupkg download. + if (Test-Path $tempPackagePath) { + Remove-Item -Path $tempPackagePath; + } + + # Figure out if the service .exe exists before we proceed. We might be packaging up an orb provider. + $exeName = "$name.exe"; + $exeLocation = (Join-Path $codeRoot $exeName); + if (!(Test-Path $exeLocation)) { + Write-Warning "$loglead : Could not locate standard $exeName name by convention. Falling back to searching for .exe's in the tools directory." + + # Search the code root for a .exe. + $executableSearch = Get-ChildItem -Path $codeRoot -Filter "*.exe" | Where-Object { $_.Name -notlike "*.vshost.exe" }; + if (($null -eq $executableSearch)) { + # Fail if we could not find any executable. + throw "$loglead : Could not locate microservice executable. Are you sure this is a microservice?"; + } elseif ($executableSearch.Count -gt 1) { + # Fail if we found more than one executable in this folder. + throw "$loglead : Located more than one microservice executable. Cannot assume which executable to use. Investigate assumptions made."; + } + $exeLocation = $executableSearch.FullName + $exeName = $executableSearch.Name + Write-Verbose "$loglead : Found executable $exeName"; + } + + # Determine if the config defaults folder has been configured. + $hasConfigDefaults = (!([string]::IsNullOrWhiteSpace($defaultLogConfigFolder))) -and (Test-Path $defaultLogConfigFolder) + + # If the config defaults folder was specified, see if the microservice should have new relic enabled/disabled. + if($hasConfigDefaults) { + + # Read in the package names to leave new-relic enabled for. + $newRelicServicePath = (Join-Path $defaultLogConfigFolder "NewRelicMicroServices/NewRelicMicroServices.txt") + + $newRelicMicroservicesToLeaveEnabled = $null + if(Test-Path $newRelicServicePath) { + $newRelicMicroservicesToLeaveEnabled = [array](Get-Content -Path $newRelicServicePath) + } else { + Write-Warning "$loglead : Could not find NewRelicMicroServices.txt to leave new relic enabled. Assuming no microservices should have NewRelic enabled." + $newRelicMicroservicesToLeaveEnabled = $null + } + + $enableNewRelic = ($newRelicMicroservicesToLeaveEnabled -contains $name) + Set-ChocolateyPackageNewRelicState -Directory $codeRoot -Enabled $enableNewRelic + } + + # If the config defaults folder was specified, look for a log4net config for this microservice. + if ($hasConfigDefaults) { + + Write-Verbose "$loglead : Config default folder exists, looking for $name log4net default." + + $configDefaultsLog4Net = (Join-Path $defaultLogConfigFolder "log4net") + $logConfigPath = Get-DefaultLog4NetPathForPackage -SourcePath $configDefaultsLog4Net -PackageName $name -EnvironmentName $environmentShortName -EnvironmentType $environmentType + Write-Verbose "$loglead : Looking for log4net default config path at '$logConfigPath'" + + # If the log4net config path exists for the microservice we're deploying, replace the one in the package! + if ((!([String]::IsNullOrEmpty($logConfigPath))) -and (Test-Path $logConfigPath)) { + $destinationLog4NetPath = (Join-Path $codeRoot "log4net.config") + Write-Verbose "$loglead : Log4net config default replacement exists, replacing the config from the package." + Copy-Item -Path $logConfigPath -Destination $destinationLog4NetPath -Force + } else { + Write-Verbose "$loglead : There is no log4net config default. Using the log4net.config from the package." + } + } + + # Copy config template files to the appropriate locations. + $applicationConfigPath = (Join-Path $outputFolder "ApplicationManifest.xml"); + $serviceConfigPath = (Join-Path $serviceRoot "ServiceManifest.xml"); + $settingsConfigPath = (Join-Path $configRoot "Settings.xml"); + Copy-Item -Path (Join-Path $fabricConfigTemplatesPath "ApplicationManifest.xml") -Destination $applicationConfigPath -Force + Copy-Item -Path (Join-Path $fabricConfigTemplatesPath "ServiceManifest.xml") -Destination $serviceConfigPath -Force + Copy-Item -Path (Join-Path $fabricConfigTemplatesPath "Settings.xml") -Destination $settingsConfigPath -Force + + # Determine the number of instances to run for a service. -1 means it runs on every node. + $instanceCount = $defaultInstanceCount; + $everywhereServices = $Global:InfrastructureMicroServices; + if ($null -ne ($everywhereServices | Where-Object { $_ -like $name })) { + $instanceCount = -1; + } + + # Detect which user to run the application as by reading the nuspec for a dependency to the database migrator. + $nuspecPath = (Get-ChildItem -Path $codeRoot -Filter "*.nuspec" | Select-Object -First 1).FullName; + + $hasMigrations = $true; + if (($null -ne $nuspecPath) -and (Test-Path $nuspecPath)) { + $nuspec = [xml](Get-Content -Path $nuspecPath); + $hasMigrations = (Test-IsPackageDbms -nuspec $nuspec); + $nuspec = $null; + } + # Else we assume the package has migrations just to be conservative. + + $gmsaUser = if($hasMigrations) { $userDbms; } else { $userMicro; } + + # Fill out the application manifest. + Write-Verbose "$loglead : Creating Application Manifest at `"$applicationConfigPath`""; + $applicationManifest = (Read-XMLFile -xmlPath $applicationConfigPath); + $applicationManifestNS = $applicationManifest.DocumentElement.NamespaceURI + $root = $applicationManifest.ApplicationManifest; + $root.SetAttribute("ApplicationTypeName", $applicationTypeName); + $root.SetAttribute("ApplicationTypeVersion", $version); + $instanceCountNode = $root.Parameters.ChildNodes | Where-Object { $_.Name -eq "InstanceCount" }; + $instanceCountNode.SetAttribute("DefaultValue", $instanceCount); + $root.ServiceManifestImport.ServiceManifestRef.SetAttribute("ServiceManifestName", $serviceName); + $root.ServiceManifestImport.ServiceManifestRef.SetAttribute("ServiceManifestVersion", $version); + $root.DefaultServices.Service.SetAttribute("Name", $serviceName); + $root.DefaultServices.Service.StatelessService.SetAttribute("ServiceTypeName", $serviceName); + $root.Principals.Users.User.SetAttribute("AccountName", $gmsaUser); + if ($null -ne $root.DefaultServices.Service.StatelessService.PlacementConstraints) { + $environmentWorkerNodeType = (Format-AlkamiEnvironmentWorkerNodeType $environmentName); + $root.DefaultServices.Service.StatelessService.PlacementConstraints = "NodeType == $environmentWorkerNodeType"; + } + + # Set nuget package name as a property. + $nugetPackageNameNode = $root.Parameters.ChildNodes | Where-Object { $_.Name -eq "NugetPackageName" } | Select-Object -First 1; + if ($null -eq $nugetPackageNameNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter NugetPackageName, adding.."; + $nugetPackageNameNode = $applicationManifest.CreateElement("Parameter",$applicationManifestNS); + [void]$root.Parameters.AppendChild($nugetPackageNameNode); + } + $nugetPackageNameNode.SetAttribute("Name", "NugetPackageName"); + $nugetPackageNameNode.SetAttribute("DefaultValue", $name); + + Save-XMLFile -xmlPath $applicationConfigPath -xml $applicationManifest; + + $root = $null; + $applicationManifest = $null; + + # Fill out the service manifest. + Write-Verbose "$loglead : Creating Service Manifest at `"$serviceConfigPath`""; + $serviceManifest = (Read-XMLFile -xmlPath $serviceConfigPath); + $serviceManifestNS = $serviceManifest.DocumentElement.NamespaceURI + $root = $serviceManifest.ServiceManifest; + $root.SetAttribute("Name", $serviceName); + $root.SetAttribute("Version", $version); + $root.ServiceTypes.StatelessServiceType.SetAttribute("ServiceTypeName", $serviceName); + $root.CodePackage.SetAttribute("Version", $version); + $root.ConfigPackage.SetAttribute("Version", $version); + $root.CodePackage.EntryPoint.ExeHost.Program = $exeName; + if ($exeName -eq "Alkami.Services.Subscriptions.Host.exe") { + $elem = $servicemanifest.CreateElement("ConsoleRedirection",$serviceManifestNS) + $root.CodePackage.EntryPoint.ExeHost.AppendChild($elem) + $elem.SetAttribute("FileRetentionCount", '5') + $elem.SetAttribute("FileMaxSizeInKb", '2048') + } + $root.Resources.Endpoints.Endpoint.SetAttribute("Name", "$($name).HostEndpoint"); + Save-XMLFile -xmlPath $serviceConfigPath -xml $serviceManifest; + $root = $null; + $serviceManifest = $null; + } catch { + Write-Error "$loglead : Failed to create Service Fabric package:`n $($_.Exception.Message)"; + + # Clean up incomplete package build. + Start-Sleep -Seconds 1; + if (Test-Path $outputFolder) { + Write-Warning "$loglead : Removing incomplete package creation at $outputFolder"; + Remove-Item $outputFolder -Recurse -Force; + } + } + + Write-Host "$loglead : Package creation complete at `"$outputFolder`"."; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricReliableServicePackage.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricReliableServicePackage.ps1 new file mode 100644 index 0000000..c50ef39 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/New-AlkamiServiceFabricReliableServicePackage.ps1 @@ -0,0 +1,303 @@ +function New-AlkamiServiceFabricReliableServicePackage { +<# +.SYNOPSIS + Prepares ReliableService implementations for deployment into a target environment. + Note: The output folder is -not- automatically cleaned up! Delete it so avoid piling up memory. +.PARAMETER source + The nuget URL of the package repository. +.PARAMETER name + The name/ID of the package to be created. +.PARAMETER version + The version of the package to be created. +.PARAMETER userMicro + The name of the non-database microservice user gmsa account. +.PARAMETER userDbms + The name of the database microservice user gmsa account. +.PARAMETER defaultInstanceCount + The default number of microservice instances that will be deployed. +.PARAMETER outputFolder + The download/output directory of the package. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("s")] + [string]$source, + [Parameter(Mandatory = $true)] + [Alias("n")] + [string]$name, + [Parameter(Mandatory = $true)] + [Alias("v")] + [string]$version, + [Parameter(Mandatory = $true)] + [string]$environmentType, + [Parameter(Mandatory = $true)] + [string]$userMicro, + [Parameter(Mandatory = $true)] + [string]$userDbms, + [Parameter(Mandatory = $true)] + [int]$defaultInstanceCount, + [Parameter(Mandatory = $true)] + [Alias("o")] + [string]$outputFolder, + [Parameter(Mandatory = $false)] + [string]$defaultLogConfigFolder = $null, + [Parameter(Mandatory = $false)] + [switch]$force, + [Parameter(Mandatory = $false)] + [pscredential]$nugetCredential = $null + ) + + $loglead = Get-LogLeadName; + $packageString = "$name|$version"; + + Write-Host "$loglead : Creating Service Fabric package for $packageString"; + + # Download Alkami package. + if(Test-Path $outputFolder) { + if(!$force) { + throw "$loglead : Directory `"$outputFolder`" already exists. If you intend to overwrite it use -force."; + } else { + Write-Host "$loglead : Recreating directory $outputFolder"; + Remove-Item -Path $outputFolder -Recurse -Force; + New-Item -Path $outputFolder -ItemType Directory | Out-Null; + } + } else { + Write-Host "$loglead : Creating directory $outputFolder"; + New-Item -Path $outputFolder -ItemType Directory | Out-Null; + } + + $tempPackagePath = (Join-Path $outputFolder "nugetDownload.zip"); + + try { + # Download specified package from nuget feed. + Write-Verbose "$loglead : Downloading package $packageString from proget to `"$tempPackagePath.`""; + Get-PackageFromProget -source $source -name $name -version $version -output $tempPackagePath -nugetCredential $nugetCredential; + + $serviceName = "svc"; + # Note: The service name "svc" is for short service fabric pathing. This is not actually the name of the service. + # The full name of the service will look like this: "Alkami.Widget.Sample/v1/svc/" + + # Unzip the .nupkg so we can manipulate manifests. + Write-Verbose "$loglead : Unzipping $packageString from `"$tempPackagePath`" to `"$outputFolder`""; + $szPath = Get-7ZipPath + & $szPath x "$tempPackagePath" -aoa -o"$outputFolder" "*" -r | Out-Null + + # Clean up nuget .nupkg download. + if(Test-Path $tempPackagePath) { + Remove-Item -Path $tempPackagePath; + } + + $codeRoot = Join-Path $outputFolder "svc\Code" + + # Make sure that this is actually a reliable service. + $files = Get-ChildItem $outputFolder -Include "*.xml" -Recurse; + $applicationConfigPath = ($files | Where-Object {$_.Name -eq "ApplicationManifest.xml"}) | Select-Object -First 1; + $serviceConfigPath = ($files | Where-Object {$_.Name -eq "ServiceManifest.xml"}) | Select-Object -First 1; + $hasApplicationManifest = $null -ne $applicationConfigPath; + $hasServiceManifest = $null -ne $serviceConfigPath; + if(!($hasApplicationManifest -and $hasServiceManifest)) { + throw "$loglead : $name-$version is not a valid Service Fabric Reliable Service"; + } + + # Determine if the config defaults folder has been configured. + $hasConfigDefaults = (!([string]::IsNullOrWhiteSpace($defaultLogConfigFolder))) -and (Test-Path $defaultLogConfigFolder) + + # If the config defaults folder was specified, see if the microservice should have new relic enabled/disabled. + if($hasConfigDefaults) { + + # Read in the package names to leave new-relic enabled for. + $newRelicServicePath = (Join-Path $defaultLogConfigFolder "NewRelicMicroServices/NewRelicMicroServices.txt") + + $newRelicMicroservicesToLeaveEnabled = $null + if(Test-Path $newRelicServicePath) { + $newRelicMicroservicesToLeaveEnabled = [array](Get-Content -Path $newRelicServicePath) + } else { + Write-Warning "$loglead : Could not find NewRelicMicroServices.txt to leave new relic enabled. Assuming no microservices should have NewRelic enabled." + $newRelicMicroservicesToLeaveEnabled = $null + } + + $enableNewRelic = ($newRelicMicroservicesToLeaveEnabled -contains $name) + Set-ChocolateyPackageNewRelicState -Directory $codeRoot -Enabled $enableNewRelic + } + + # If the log4net config defaults folder was specified, look for a config for this service. + if($hasConfigDefaults) { + + Write-Verbose "$loglead : Config default folder exists, looking for $name log4net default." + + $configDefaultsLog4Net = (Join-Path $defaultLogConfigFolder "log4net") + $logConfigPath = Get-DefaultLog4NetPathForPackage -SourcePath $configDefaultsLog4Net -PackageName $name -EnvironmentName $environmentShortName -EnvironmentType $environmentType -IsReliableService + Write-Verbose "$loglead : Looking for log4net default config path at '$logConfigPath'" + + # If the log4net config path exists for the service we're deploying, replace the one in the package! + if ((!([String]::IsNullOrEmpty($logConfigPath))) -and (Test-Path $logConfigPath)) { + $destinationLog4NetPath = (Join-Path $codeRoot "log4net.config") + Write-Verbose "$loglead : Log4net config default replacement exists, replacing the config from the package." + Copy-Item -Path $logConfigPath -Destination $destinationLog4NetPath -Force + } else { + Write-Verbose "$loglead : There is no log4net config default. Using the log4net.config from the package." + } + } + + # Determine the number of instances to run for a service. -1 means it runs on every node. + $instanceCount = $defaultInstanceCount; + $everywhereServices = $Global:InfrastructureMicroServices; + if($null -ne ($everywhereServices | Where-Object {$_ -like $name})) { + $instanceCount = -1; + } + + # Detect which user to run the application as by reading the nuspec for a dependency to the database migrator. + $nuspecPath = (Get-ChildItem -Path $codeRoot -Filter "*.nuspec" | Select-Object -First 1).FullName; + + $hasMigrations = $true; + if(($null -ne $nuspecPath) -and (Test-Path $nuspecPath)) { + $nuspec = [xml](Get-Content -Path $nuspecPath); + $hasMigrations = (Test-IsPackageDbms -nuspec $nuspec); + $nuspec = $null; + } + # Else we assume the package has migrations just to be conservative. + + $gmsaUser = if($hasMigrations) { $userDbms; } else { $userMicro; } + + # Fill out the application manifest. + Write-Verbose "$loglead : Creating Application Manifest at `"$applicationConfigPath`""; + $applicationManifest = (Read-XMLFile -xmlPath $applicationConfigPath); + $root = $applicationManifest.ApplicationManifest; + $root.SetAttribute("ApplicationTypeVersion", $version); + + $root.ServiceManifestImport.ServiceManifestRef.SetAttribute("ServiceManifestName",$serviceName); + $root.ServiceManifestImport.ServiceManifestRef.SetAttribute("ServiceManifestVersion",$version); + $root.DefaultServices.Service.SetAttribute("Name", $serviceName); + $root.DefaultServices.Service.StatelessService.SetAttribute("InstanceCount", $instanceCount); + + # Make sure that the reliable service is not deployed on the seed nodes. + $placementConstraintNode = $root.DefaultServices.Service.StatelessService.ChildNodes | Where-Object {$_.Name -eq "PlacementConstraints"} | Select-Object -First 1; + if($null -eq $placementConstraintNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter PlacementConstraints, adding.."; + $placementConstraintNode = $applicationManifest.CreateElement("PlacementConstraints"); + [void]$root.DefaultServices.Service.StatelessService.AppendChild($placementConstraintNode); + } + $placementConstraintNode.InnerText = "NodeType != SeedNode"; + + # Set certificate properties. + $certificate = $null; + $reverseProxyUrl = $null; + $isDevEnvironment = ($environmentType -eq "Development"); + $isQaEnvironment = ($environmentType -eq "QA"); + $isStagingEnvironment = ($environmentType -eq "Staging"); + $isProductionEnvironment = ($environmentType -eq "Production"); + + # Only modify the certificate properties if it is NOT a qa environment. QA environment properties should just be passed through. + # This should eventually change with certificate conventions defined for each environment. + if(!$isQaEnvironment) { + if($isDevEnvironment) { + $certificate = "*.dev.alkamitech.com"; + $reverseProxyUrl = "https://microservices.dev.alkamitech.com:19081"; + } elseif($isStagingEnvironment) { + $certificate = "*.staging.alkamitech.com"; + $reverseProxyUrl = "https://microservices.staging.alkamitech.com:19081"; + } elseif($isProductionEnvironment) { + $certificate = "api.alkami.com" # Has a SAN for tde.prod.alkami.net + $reverseProxyUrl = "https://tde.prod.alkami.net:19081" + } + + # Throw an exception if the environment type did not successfully set a certificate to use / reverse proxyu URL. + if(($null -eq $certificate) -or ($null -eq $reverseProxyUrl)) { + throw "Unhandled environment type $environmentType. Certificate and reverse proxy url is undefined."; + } + + # Set certificate common name. + $certNameNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -eq "CertificateCommonName"} | Select-Object -First 1; + if($null -eq $certNameNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter CertificateCommonName, adding.."; + $certNameNode = $applicationManifest.CreateElement("Parameter"); + [void]$root.Parameters.AppendChild($certNameNode); + } + $certNameNode.SetAttribute("Name", "CertificateCommonName"); + $certNameNode.SetAttribute("DefaultValue", $certificate); + + # Set JWT certificate name. + $jwtCertNameNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -eq "JWTCertificateCommonName"} | Select-Object -First 1; + if($null -eq $jwtCertNameNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter JWTCertificateCommonName, adding.."; + $jwtCertNameNode = $applicationManifest.CreateElement("Parameter"); + [void]$root.Parameters.AppendChild($jwtCertNameNode); + } + $jwtCertNameNode.SetAttribute("Name", "JWTCertificateCommonName"); + $jwtCertNameNode.SetAttribute("DefaultValue", $certificate); + + # Set the reverse proxy URL. + $reverseProxyNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -eq "ReverseProxyUrl"} | Select-Object -First 1; + if($null -eq $reverseProxyNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter ReverseProxyUrl, adding.."; + $reverseProxyNode = $applicationManifest.CreateElement("Parameter"); + [void]$root.Parameters.AppendChild($reverseProxyNode); + } + $reverseProxyNode.SetAttribute("Name", "ReverseProxyUrl"); + $reverseProxyNode.SetAttribute("DefaultValue", $reverseProxyUrl); + } + + # Set nuget package name as a property so we have a mapping from contract name -> nuget package name. + $nugetPackageNameNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -eq "NugetPackageName"} | Select-Object -First 1; + if($null -eq $nugetPackageNameNode) { + Write-Verbose "$loglead Could not find ApplicationManifest parameter NugetPackageName, adding.."; + $nugetPackageNameNode = $applicationManifest.CreateElement("Parameter"); + [void]$root.Parameters.AppendChild($nugetPackageNameNode); + } + $nugetPackageNameNode.SetAttribute("Name", "NugetPackageName"); + $nugetPackageNameNode.SetAttribute("DefaultValue", $name); + + # Set the gmsa account to be used to run the service. + $serviceAccountNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -eq "ServiceAccountToRunAs"}; + if($null -eq $serviceAccountNode) { + throw "$loglead Could not find ServiceAccountToRunAs parameter. This must exist in the manifest!"; + } else { + Write-Verbose "$loglead Setting ServiceAccountToRunAs parameter to $gmsaUser"; + $serviceAccountNode.SetAttribute("DefaultValue", $gmsaUser); + } + + # Make sure the gmsa policy section exists. + $policyNode = $root.ServiceManifestImport.Policies.RunAsPolicy; + if(($null -eq $policyNode) -or ($policyNode.UserRef -ne "DomainGMSA")) { + throw "$loglead Could not find gmsa RunAsPolicy section. This must exist in the manifest!"; + } + + # Set the environment type if there is one. + $environmentTypeNode = $root.Parameters.ChildNodes | Where-Object {$_.Name -like "*ASPNETCORE_ENVIRONMENT"}; + if($null -ne $environmentTypeNode) { + Write-Verbose "$loglead Setting ASPNETCORE_ENVIRONMENT type setting to $environmentType" + $environmentTypeNode.SetAttribute("DefaultValue", $environmentType); + } + + Save-XMLFile -xmlPath $applicationConfigPath -xml $applicationManifest; + + # INFO: Powershell adds empty namespace attributes 'xmlns=""' to new elements. Remove those. There's probably a better way to do this. + (Get-Content -Path $applicationConfigPath).Replace("xmlns=`"`"","") | Set-Content -Path $applicationConfigPath; + $root = $null; + $applicationManifest = $null; + + # Fill out the service manifest. + Write-Verbose "$loglead : Creating Service Manifest at `"$serviceConfigPath`""; + $serviceManifest = (Read-XMLFile -xmlPath $serviceConfigPath); + $root = $serviceManifest.ServiceManifest; + $root.SetAttribute("Name", $serviceName); + $root.SetAttribute("Version", $version); + $root.CodePackage.SetAttribute("Version", $version); + $root.ConfigPackage.SetAttribute("Version", $version); + Save-XMLFile -xmlPath $serviceConfigPath -xml $serviceManifest; + $root = $null; + $serviceManifest = $null; + } catch { + Write-Error "$loglead : Failed to create Service Fabric package:`n $($_.Exception.Message)"; + + # Clean up incomplete package build. + Start-Sleep -Seconds 1; + if(Test-Path $outputFolder) { + Write-Warning "$loglead : Removing incomplete package creation at $outputFolder"; + Remove-Item $outputFolder -Recurse -Force; + } + } + + Write-Host "$loglead : Package creation complete at `"$outputFolder`"."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Open-ServiceFabricDashboard.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Open-ServiceFabricDashboard.ps1 new file mode 100644 index 0000000..bee7a3c --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Open-ServiceFabricDashboard.ps1 @@ -0,0 +1,16 @@ +function Open-ServiceFabricDashboard { +<# +.SYNOPSIS + Opens the Service Fabric dashboard on the local machine, or for a remote machine. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param() + + $siteName = (Get-AlkamiServiceFabricServerCertificateName); + + $url = "http://$($siteName):19080" + Start-Process $url + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.Tests.ps1 new file mode 100644 index 0000000..45a7b60 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.Tests.ps1 @@ -0,0 +1,133 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +# Make tests not fail if ServiceFabric SDK not installed +$serviceFabricSDKIsInstalled = (Test-Path "C:\Program Files\Microsoft Service Fabric") +if (!$serviceFabricSDKIsInstalled) { + + Write-Warning "The Service Fabric SDK must be installed to execute these tests. They will be skipped" +} + +Describe "Publish-AlkamiServiceFabricPackages" { + $podName = "AWS Production 5"; + + Mock Get-AppSetting -ModuleName $moduleForMock { + param($appSettingKey, $filePath, $computername,$SuppressWarnings) + + if($appSettingKey -eq "Environment.Name") { + return $podName; + } + if($appSettingKey -eq "Environment.Type") { + return "FakeEnvironment"; + } + return "FakeValue"; + } + + # Setup + $packageString = "Alkami.Fake.Package 1.2.3"; + $packages = Format-ParseChocoPackages -text $packageString -delimiter " "; + foreach($package in $packages) { + $package.Feed = @{ Source = "https://www.fakesource.com/nuget/choco.fake" }; + } + $podShortName = Format-AlkamiEnvironmentName -name $podName; + + Context "Deploy Packages" { + + if ($serviceFabricSDKIsInstalled) { + + # Inside IF statement to avoid errors mocking functions that don't exist + Mock Remove-Item -ModuleName $moduleForMock { } + Mock Set-ChocoPackageSourceFeeds -ModuleName $moduleForMock { } + Mock Connect-AlkamiServiceFabricCluster -ModuleName $moduleForMock { } + Mock New-AlkamiServiceFabricPackage -ModuleName $moduleForMock { } + Mock New-AlkamiServiceFabricReliableServicePackage -ModuleName $moduleForMock { } + Mock Copy-ServiceFabricApplicationPackage -ModuleName $moduleForMock { } + Mock Register-ServiceFabricApplicationType -ModuleName $moduleForMock { } + Mock Format-AlkamiServiceFabricApplicationName -ModuleName $moduleForMock { } + Mock Test-IsPackageReliableService -ModuleName $moduleForMock { } + Mock Get-AlkamiServiceFabricApplications -Module $moduleForMock { return $null } + + Mock Get-AlkamiServiceFabricNode -ModuleName $moduleForMock { + $numServers = 5; + $results = @(); + for($i = 0; $i -lt $numServers; $i++) { + $results += "FABFAKE$i"; + } + return $results; + } + + Mock Get-AlkamiServiceFabricPackageServiceTypeName -ModuleName $moduleForMock { + return "Alkami.Fake.Package"; + } + + Mock Get-ServiceFabricApplicationType -ModuleName $moduleForMock { + return $null; + } + + Mock Test-IsPackageReliableService -ModuleName $moduleForMock { + return $true; + } + + Mock Get-ServiceFabricNode -ModuleName $moduleForMock { + return @("APP12345.fh.local"); + } + } + + It "Correctly Builds a Reliable Service Package" { + + if (!$serviceFabricSDKIsInstalled) { + + Set-ItResult -Inconclusive -Because "Service Fabric SDK Not Installed" + + } else { + + Mock Test-IsPackageReliableService -ModuleName $moduleForMock { + return $true; + } + + Publish-AlkamiServiceFabricPackages -packages $packages; + Assert-MockCalled -ModuleName $moduleForMock New-AlkamiServiceFabricReliableServicePackage -Times 1 -Exactly -Scope It; + } + } + + It "Correctly Builds a Microservice Package" { + + if (!$serviceFabricSDKIsInstalled) { + + Set-ItResult -Inconclusive -Because "Service Fabric SDK Not Installed" + + } else { + + Mock Test-IsPackageReliableService -ModuleName $moduleForMock { + return $false; + } + + Publish-AlkamiServiceFabricPackages -packages $packages; + Assert-MockCalled -ModuleName $moduleForMock New-AlkamiServiceFabricPackage -Times 1 -Exactly -Scope It; + Assert-MockCalled -ModuleName $moduleForMock Format-AlkamiServiceFabricApplicationName -Times 1 -ParameterFilter {![string]::IsNullOrWhiteSpace($environmentName)} -Exactly -Scope It; + } + } + + It "Registers Image with Full Name/Version/Environment" { + + if (!$serviceFabricSDKIsInstalled) { + + Set-ItResult -Inconclusive -Because "Service Fabric SDK Not Installed" + + } else { + + $podShortName = Format-AlkamiEnvironmentName -name $podName; + + Publish-AlkamiServiceFabricPackages -packages $packages; + $package = $packages[0]; + $imagePathName = "$($package.Name)-$($package.Version)-$podShortName"; + Assert-MockCalled -ModuleName $moduleForMock Copy-ServiceFabricApplicationPackage -Times 1 #-ParameterFilter {$ApplicationPackagePathInImageStore -eq "$imagePathName"} -Exactly -Scope It; + } + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.ps1 new file mode 100644 index 0000000..90eba6f --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Publish-AlkamiServiceFabricPackages.ps1 @@ -0,0 +1,181 @@ +function Publish-AlkamiServiceFabricPackages { +<# +.SYNOPSIS + Stages Microservice Packages to the service fabric cluster endpoint before deployments. +.PARAMETER packages + The package objects to be staged onto the service fabric cluster. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [Alias("PackagesToPublish")] + [AllowNull()] + [object[]]$packages, + + [Parameter(Mandatory = $false)] + [Alias("s")] + [string]$hostname = "localhost", + + [Parameter(Mandatory = $false)] + [string]$defaultLogConfigFolder = $null, + [Parameter(Mandatory = $false)] + [pscredential]$NugetCredential + ) + + $loglead = Get-LogLeadName; + $StopWatch = [System.Diagnostics.Stopwatch]::StartNew(); + + # Set feeds to packages if they are not already there. + Set-ChocoPackageSourceFeeds -packages $packages -hostname $hostname -Verbose; + + # Remove installers from the list. + $packages = $packages | Where-Object { !(Test-IsPackageInstaller -packageName $_.Name) }; + + Write-Host "$loglead : Looking up information on remote host $hostname"; + + # Connect to SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + # Get machine config AppSettings. + $environmentName = Get-AppSetting -appSettingKey "Environment.Name" -ComputerName $hostname; + $environmentType = Get-AppSetting -appSettingKey "Environment.Type" -ComputerName $hostname; + $dbUser = Get-AppSetting -appSettingKey "DatabaseMicroServiceAccount" -ComputerName $hostname; + $microUser = Get-AppSetting -appSettingKey "NonDatabaseMicroServiceAccount" -ComputerName $hostname; + $environmentShortName = Format-AlkamiEnvironmentName -name $environmentName; + + # Get the server nodes relevant to the environment. + $nodes = Get-AlkamiServiceFabricNode -EnvironmentName $environmentName; + if(Test-IsCollectionNullOrEmpty $nodes) { + throw "$loglead : Could not identify environment Service Fabric nodes for $environmentShortName"; + } + $nodeCount = $nodes.count; + + # Determine the default instance count of microservices based on the environment. + # Make sure there are at least two instances, unless it's a single server environment. + $defaultInstanceCount = [System.Math]::Max($nodeCount-2, 2); + if($nodeCount -eq 1) { + $defaultInstanceCount = 1; + } + + # Get the running applications on the cluster. Note: This is not the list of registered package images. + $runningApplications = Get-AlkamiServiceFabricApplications -ComputerName $hostname -EnvironmentName $environmentName; + + # Create and register applications. + foreach($package in $packages) { + $packageStopWatch = [System.Diagnostics.Stopwatch]::StartNew(); + $name = $package.Name; + $version = $package.Version; + + $hasError = $false; + + # Try to find/create the package from each of the choco feeds configured on the remote server. + $packageSource = $package.Feed.Source; + if([string]::IsNullOrWhiteSpace($packageSource)) { + throw "$loglead : Unable to locate chocolatey feed containing package $name|$version on remote computer $hostname"; + } + + # Determine if the package is a SF reliable service vs a normal microservice. + $isReliableService = (Test-IsPackageReliableService -feedSource $packageSource -name $name -version $version -Credential $NugetCredential); + + # Determine application name and application type name. + $applicationTypeName = $null; + if($isReliableService) { + # The application type name -must- be the name of the class that implements the reliable service contract. + # Pull this name from the manifest located inside the package. + $applicationTypeName = Get-AlkamiServiceFabricPackageServiceTypeName -source $packageSource -name $name -version $version -NugetCredential $NugetCredential + } else { + # For normal microservices the application type name is environment tied, and the application name matches the service name. + $applicationTypeName = Format-AlkamiServiceFabricApplicationName -name $name -environmentName $environmentName; + } + + # Build the full name for SF image versioning purposes. + $fullNameVersion = "$name-$version-$environmentShortName"; + + Write-Verbose "$loglead : Determining if `"$name|$version`" is already registered on the cluster."; + $registrationInfo = Get-ServiceFabricApplicationType -ApplicationTypeName $applicationTypeName -ApplicationTypeVersion $version; + if(!($registrationInfo)) { + + # Determine the number of instances to deploy the application with. + $instanceCount = $defaultInstanceCount; + + # Figure out if another version of the service is already running on the cluster, and set the instance count to what it currently is. + $applicationSearch = $runningApplications | Where-Object {$_.Name -eq $name} | Select-Object -First 1; + if($null -ne $applicationSearch) { + $instanceCount = Get-AlkamiServiceFabricApplicationInstanceCount -ServiceFabricApplicationName $applicationSearch.ServiceFabricApplicationName -ComputerName $hostname; + } + + # Create temporary folder location. + $tempOutputFolder = [System.IO.Path]::GetTempPath() + [guid]::NewGuid().ToString(); + try { + if($isReliableService) { + # Poke required values into reliable service app/svc manifests where appropriate. + $arguments = @{ + source = $packageSource + name = $name + version = $version + outputFolder = $tempOutputFolder + userDbms = $dbUser + userMicro = $microUser + defaultInstanceCount = $instanceCount + environmentType = $environmentType + defaultLogConfigFolder = $defaultLogConfigFolder + NugetCredential = $NugetCredential + + } + New-AlkamiServiceFabricReliableServicePackage @arguments; + } else { + # Repackage topshelf microservice to the format that service fabric expects.. + $arguments = @{ + source = $packageSource + name = $name + version = $version + outputFolder = $tempOutputFolder + userDbms = $dbUser + userMicro = $microUser + defaultInstanceCount = $instanceCount + environmentName = $environmentName + environmentType = $environmentType + defaultLogConfigFolder = $defaultLogConfigFolder + NugetCredential = $NugetCredential + } + New-AlkamiServiceFabricPackage @arguments; + } + + # Copy package to image store. + Write-Host "$loglead : Uploading and Registering $fullNameVersion from package built at $tempOutputFolder"; + Copy-ServiceFabricApplicationPackage -ApplicationPackagePath $tempOutputFolder -ApplicationPackagePathInImageStore $fullNameVersion -CompressPackage; + + # Register the application with Service Fabric. + Register-ServiceFabricApplicationType -ApplicationPathInImageStore $fullNameVersion -ApplicationPackageCleanupPolicy Automatic; + + Write-Host "$loglead : Package publish complete."; + } catch { + Write-Error "$loglead : Failed to publish Service Fabric package:`n $($_.Exception.Message)"; + $hasError = $true; + } finally { + if(Test-Path $tempOutputFolder) { + if($hasError) { + Write-Warning "$loglead : Removing incomplete package creation at $tempOutputFolder"; + } else { + Write-Host "$loglead : Removing package files at $tempOutputFolder"; + } + + Invoke-CommandWithRetry -MaxRetries 3 -SecondsDelay 1 -Arguments @($tempOutputFolder) -ScriptBlock { + param($folderPath) + Remove-FileSystemItem $folderPath -Recurse + } + } + } + } + else { + Write-Host "$loglead : Service Fabric Application $name|$version is already registered. Skipping."; + } + $packageStopWatch.Stop(); + Write-Verbose "$loglead : Completed $name|$version in $($packageStopWatch.Elapsed.TotalSeconds) seconds."; + } + + $StopWatch.Stop(); + Write-Verbose "$loglead : Completed in $($StopWatch.Elapsed.TotalSeconds) seconds."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplication.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplication.ps1 new file mode 100644 index 0000000..3fd73e8 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplication.ps1 @@ -0,0 +1,69 @@ +function Remove-AlkamiServiceFabricApplication { +<# +.SYNOPSIS + Removes applications from the service fabric cluster. +.PARAMETER packages + One or more packages to remove from the cluster. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +.PARAMETER Deregister + Deregisters the image the service is using, for a fresh deployment next time. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [AllowNull()] + [object[]]$Packages, + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost", + [Parameter(Mandatory = $false)] + [switch]$Deregister + ) + + $loglead = Get-LogLeadName; + + if(Test-IsCollectionNullOrEmpty $Packages) { + Write-Verbose "$loglead There were no packages to remove. Returning.."; + return; + } + + # Connect to SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $Hostname | Out-Null; + $applications = Get-AlkamiServiceFabricApplications -ComputerName $Hostname; + + Write-Verbose "$loglead Searching for packages to remove:"; + foreach($package in $Packages) { + Write-Verbose "$loglead $($package.Name) $($package.Version)"; + } + + # Filter down the applications to the packages we are actually removing. + $applicationsToRemove = @(); + foreach($package in $Packages) { + # Just remove the package if a version isn't specified, or otherwise a specific version if it is specified. + $applicationToRemove = $applications | Where-Object { ($_.Name -eq $package.Name) -and (($null -eq $package.Version) -or $($_.Version -eq $package.Version)); }; + if($null -ne $applicationToRemove) { + $applicationsToRemove += $applicationToRemove; + } + } + + if(Test-IsCollectionNullOrEmpty $applicationsToRemove) { + Write-Verbose "$loglead Found no matching packages to remove. Returning.."; + return; + } + + Write-Verbose "$loglead Starting removal of $($Packages.Count) package(s)."; + + # Remove all of the relevant SF applications. + foreach($application in $applicationsToRemove) { + + Write-Host "$loglead Removing $($application.ServiceFabricApplicationName)"; + Remove-ServiceFabricApplication -ApplicationName $application.ServiceFabricApplicationName -Force | Out-Null; + + if($Deregister.IsPresent) { + Write-Host "$loglead Unregistering $($application.ServiceFabricApplicationTypeName)-$($application.Version)"; + Unregister-ServiceFabricApplicationType -ApplicationTypeName $application.ServiceFabricApplicationTypeName -ApplicationTypeVersion $application.Version -Force; + } + } + + Write-Verbose "$loglead Application removal complete."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplications.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplications.ps1 new file mode 100644 index 0000000..da69ecc --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricApplications.ps1 @@ -0,0 +1,64 @@ +function Remove-AlkamiServiceFabricApplications { +<# +.SYNOPSIS + Removes/unregisters all applications from a service fabric cluster. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + + [Parameter(Mandatory = $false)] + [switch]$force + ) + + $loglead = Get-LogLeadName; + + if(!($force.IsPresent)) { + throw "$loglead Must include -Force. You wouldn't want to run this accidentally, now would you?"; + return; + } + + # Connect to SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + # Attempt removing applications twice, because services that take too long to stop will time out the requests. + # The removals still happen, but there is no way to control the timeout on the command. + # Two attempts generally gets them all. + $removalAttempts = 2; + for($i = 0; $i -lt $removalAttempts; $i++) { + # Remove all applications. + $applications = Get-ServiceFabricApplication; + foreach($application in $applications) { + try { + Write-Host "$loglead Removing $($application.ApplicationName)-$($application.ApplicationTypeVersion)"; + Remove-ServiceFabricApplication -ApplicationName $application.ApplicationName -Force; + } + catch { + Write-Warning $_; + } + } + + # Unregister all applications. + $registeredApps = Get-ServiceFabricApplicationType; + foreach($application in $registeredApps) { + try { + Write-Host "$loglead Unregistering $($application.ApplicationTypeName)-$($application.ApplicationTypeVersion)"; + Unregister-ServiceFabricApplicationType -ApplicationTypeName $application.ApplicationTypeName -ApplicationTypeVersion $application.ApplicationTypeVersion -Force; + } + catch { + Write-Warning $_; + } + } + } + + # Throw an exception if the cluster was not completely wiped. + $applications = Get-ServiceFabricApplication; + $registeredApps = Get-ServiceFabricApplicationType; + if(($applications.count -gt 0) -or ($registeredApps.count -gt 0)) { + throw "$loglead The cluster was not completely wiped of applications."; + } + Write-Host "$loglead Service Fabric application removal complete."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricCluster.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricCluster.ps1 new file mode 100644 index 0000000..d684607 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricCluster.ps1 @@ -0,0 +1,61 @@ +function Remove-AlkamiServiceFabricCluster { +<# +.SYNOPSIS + Shuts down a service fabric cluster and removes it completely. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + [Parameter(Mandatory = $false)] + [switch]$WhatIf + ) + + $loglead = Get-LogLeadName; + + if($WhatIf.IsPresent) { + Write-Host "$loglead : Running in WhatIf mode. The cluster is not actually being destroyed!"; + } + + Write-Host "$loglead : Destroying Service Fabric cluster."; + + Write-Host "$loglead : Connecting to Service Fabric cluster."; + + # Connect to SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + # TODO: Handle when the cluster is already destroyed. + + # Query all of the nodes from the cluster we're destroying. + Write-Host "$loglead : Querying sibling nodes from $hostname."; + $nodes = (Get-ServiceFabricNode); + + $names = ($nodes | select-object -ExpandProperty IpAddressOrFQDN) -join ", "; + Write-Host "$loglead : Found node(s) $names"; + + # Remove the cluster. + Write-Host "$loglead : Shutting down Service Fabric cluster."; + $clusterConfigPath = "c`$\ProgramData\SF\ClusterConfig.json"; + $configPath = "\\$hostname\$clusterConfigPath"; + if($WhatIf.IsPresent) { + Write-Host "$loglead : Mwahaha! The cluster running on $hostname and friends would have been destroyed!"; + } else { + Remove-ServiceFabricCluster -ClusterConfigurationFilePath $configPath -Force; + } + + # The object returned can't be enumerated by foreach for some reason. + foreach($node in $nodes) { + $server = $node.IpAddressOrFQDN; + $nodeName = $node.NodeName; + + $sfPath = "\\$server\C`$\ProgramData\SF\$nodeName"; + if(Test-Path $sfPath) { + Write-Host "$loglead : Removing Fabric data at `"$sfPath`""; + if(!($WhatIf.IsPresent)) { + Remove-Item $sfPath -Recurse -Force; + } + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricMultipleMajorVersions.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricMultipleMajorVersions.ps1 new file mode 100644 index 0000000..dbc5654 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Remove-AlkamiServiceFabricMultipleMajorVersions.ps1 @@ -0,0 +1,55 @@ +function Remove-AlkamiServiceFabricMultipleMajorVersions { +<# +.SYNOPSIS + Removes multiple major versions of a microservice, preserving the latest major version. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost" + ) + + $loglead = (Get-LogLeadName); + + Write-Host "$loglead Now removing multiple major microservice versions and keeping the latest version."; + + # Connect to the cluster and grab the deployed applications. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + $applications = Get-AlkamiServiceFabricApplications -ComputerName $hostname; + + # Map applications from [packageName -> list of running applications] + $appMap = @{}; + foreach($app in $applications) { + if(!($appMap.ContainsKey($app.Name))) { + $appMap[$app.Name] = @(); + } + + $appMap[$app.Name] = $appMap[$app.Name] + $app; + } + + # Foreach application running on the cluster... + $removedAny = $false; + foreach($key in $appMap.Keys) { + # Grab the services for the particular app running on the cluster. + # Sort by name, which also sorts by major version. + $services = $appMap[$key]; + $services = $services | Sort-Object -Property ServiceFabricApplicationName; + + # If there is more than one version of the service running.. + if($services.Count -gt 1) { + # Remove everything except for the last service in the list. + $removeCount = $services.Count - 1; + for($i = 0; $i -lt $removeCount; $i++) { + Write-Host "$loglead Removing $($services[$i].ServiceFabricApplicationName)"; + Remove-ServiceFabricApplication -ApplicationName ($services[$i].ServiceFabricApplicationName) -Force; + $removedAny = $true; + } + Write-Host "$loglead $($services[$removeCount].ServiceFabricApplicationName) still lives!`n"; + } + } + if(!$removedAny) { + Write-Host "$loglead Found no microservices to remove."; + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplication.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplication.ps1 new file mode 100644 index 0000000..d51fd57 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplication.ps1 @@ -0,0 +1,115 @@ +function Restart-AlkamiServiceFabricApplication { +<# +.SYNOPSIS + Restarts a microservice by chocolatey package name. +.PARAMETER name + The chocolatey/nuget name of the microservice to be restarted. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, ParameterSetName="Name")] + [string]$name, + [Parameter(Mandatory = $true, ParameterSetName="ApplicationTypeName")] + [string]$applicationTypeName, + [Parameter(Mandatory = $false, ParameterSetName="Name")] + [Parameter(Mandatory = $false, ParameterSetName="ApplicationTypeName")] + [string]$hostname = "localhost" + ) + + $loglead = (Get-LogLeadName) + + # Connect to the SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null + + $applications = $null + if(!([string]::IsNullOrWhiteSpace($name))) { + $applications = (Get-AlkamiServiceFabricApplications -Name $name -ComputerName $hostname) + } elseif(!([string]::IsNullOrWhiteSpace($applicationTypeName))) { + $applications = ((Get-AlkamiServiceFabricApplications -ComputerName $hostname) | Where-Object { $_.ServiceFabricApplicationTypeName -eq $applicationTypeName } ) + } + + if(Test-IsCollectionNullOrEmpty($applications)) { + Write-Error "$loglead : Could not find an application named $name to restart. Are you connected to a Seed Node?" + return + } + + Write-Verbose "$loglead : Found $($applications.count) Application(s) to Restart:" + foreach($application in $applications) { + Write-Verbose $application.ServiceFabricApplicationName + } + + $nodes = Get-ServiceFabricNode + $hostnameNode = $nodes | Where-Object { ($_.NodeName -like $hostname) -or ($_.IpAddressOrFQDN -like $hostname) } | Select-Object -First 1 + if($null -eq $hostnameNode) { + Write-Error "$loglead : Unable to determine an environment/NodeType associated with server $hostname" + return + } + $nodeType = $hostnameNode.NodeType + + # Filter the nodes down to the servers in the specific environment that are up. + $environmentNodes = $nodes | Where-Object { ($_.NodeType -eq $nodeType) -and ($_.NodeStatus -eq "Up") } + + # Get the node-names. + $nodeNames = $environmentNodes | Select-Object -ExpandProperty "NodeName" + + # Define script block to restart an individual application. + $restartApplicationScriptBlock = { + param($sbApplication, $sbNodeName) + + # Get applications on the node, and filter the applications to those that are active. + $applicationsOnNode = Get-ServiceFabricDeployedApplication -NodeName $sbNodeName -ApplicationName $sbApplication.ServiceFabricApplicationName + $applicationsOnNode = $applicationsOnNode.Where({ + $_.DeployedApplicationStatus -eq "Active" + }) + if(Test-IsCollectionNullOrEmpty $applicationsOnNode) { + return + } + + # In the hierarchy of Alkami-Style SF Microservices, all Application/Service services are named "svc" + $serviceName = "svc" + + foreach($nodeApp in $applicationsOnNode) { + # Get the code package and restart it. + Write-Verbose "$loglead : Restarting app $($nodeApp.ApplicationName) on $sbNodeName" + + $codePackageParams = @{ + NodeName = $sbNodeName + ApplicationName = $nodeApp.ApplicationName + ServiceManifestName = $serviceName + } + + # Get the code package for the specific node + $codePackage = Get-ServiceFabricDeployedCodePackage @codePackageParams + if($null -eq $codePackage) { + return + } + + # Restart the service! + $restartParameters = @{ + NodeName = $sbNodeName + ApplicationName = $nodeApp.ApplicationName + ServiceManifestName = $serviceName + CodePackageName = $codePackage.CodePackageName + CodePackageInstanceId = $codePackage.EntryPoint.CodePackageInstanceId + ServicePackageActivationId = $codePackage.ServicePackageActivationId + CommandCompletionMode = "Verify" + } + + Restart-ServiceFabricDeployedCodePackage @restartParameters | Out-Null + } + } + + # For each application we are restarting + foreach($application in $applications) { + Write-Host "$loglead : Restarting Application $($application.ServiceFabricApplicationName)" + + # For each node, restart that application if it exists on the node. + foreach($nodeName in $nodeNames) { + Invoke-CommandWithRetry -Arguments ($application, $nodeName) -MaxRetries 5 -Exponential -Verbose -ScriptBlock $restartApplicationScriptBlock + } + } + + Write-Host "$loglead : Rolling restarts are complete." +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplications.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplications.ps1 new file mode 100644 index 0000000..458eeec --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Restart-AlkamiServiceFabricApplications.ps1 @@ -0,0 +1,72 @@ +function Restart-AlkamiServiceFabricApplications { +<# +.SYNOPSIS + Restarts every microservice in the Service Fabric cluster. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost", + [Parameter(Mandatory = $false)] + [switch]$force + ) + + $loglead = (Get-LogLeadName); + + if(!($force.IsPresent)) { + throw "$loglead Must include -Force. You wouldn't want to run this accidentally, now would you?"; + return; + } + + # Connect to the SF cluster. + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + Write-Host "$loglead Restarting all services on Service Fabric cluster at $hostname"; + + # Get all of the SF nodes for the environment. + $nodes = Get-ServiceFabricNode; + + # Identify the node type (and thus the environment) of the node we are targeting. + if($hostname -eq "localhost") { + $hostname = Get-FullyQualifiedServerName; + } + $hostnameNode = $nodes | Where-Object { ($_.NodeName -like $hostname) -or ($_.IpAddressOrFQDN -like $hostname) } | Select-Object -First 1; + if($null -eq $hostnameNode) { + Write-Error "$loglead Unable to determine an environment/NodeType associated with server $hostname"; + return; + } + if($hostnameNode.NodeType -eq "SeedNode") { + Write-Error "$hostname is a Seed Node. Please target a worker node to bounce a specific environment."; + return; + } + $nodeType = $hostnameNode.NodeType; + + Write-Verbose "$loglead Restarting services on nodes with type $nodeType"; + + # Filter the nodes down to the servers in the specific environment, and nodes that are up. + $nodes = $nodes | Where-Object { ($_.NodeType -eq $nodeType) -and ($_.NodeStatus -eq "Up") }; + + # Get the unique applications deployed to each of these nodes. + $applications = @(); + foreach($node in $nodes) { + $applications += (Get-ServiceFabricDeployedApplication -NodeName $node.NodeName); + } + $applications = $applications | Select-Object -ExpandProperty ApplicationTypeName -Unique; + + if(Test-IsCollectionNullOrEmpty $applications) { + Write-Warning "$loglead Found no applications to restart. Exiting.."; + return; + } + + # Restart all the applications. + Invoke-Parallel -objects $applications -arguments $hostname -numThreads 16 -script { + param($applicationTypeName, $arguments) + $hostname = $arguments[0]; + + Restart-AlkamiServiceFabricApplication -applicationTypeName $applicationTypeName -hostname $hostname; + }; + + Write-Host "$loglead All services are restarted."; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Set-AlkamiServiceFabricApplicationInstanceCount.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Set-AlkamiServiceFabricApplicationInstanceCount.ps1 new file mode 100644 index 0000000..8f728f5 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Set-AlkamiServiceFabricApplicationInstanceCount.ps1 @@ -0,0 +1,36 @@ +function Set-AlkamiServiceFabricApplicationInstanceCount { +<# +.SYNOPSIS + Sets the desired instance count of a microservice. + The function fails if the service is not deployed. +.PARAMETER ServiceFabricApplicationName + The Service Fabric application name of the package to look up. +.PARAMETER DesiredInstanceCount + The desired number of instances to run the service with. +.PARAMETER ComputerName + The host name of any FAB server in the Service Fabric cluster. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$ServiceFabricApplicationName, + [Parameter(Mandatory = $true)] + [string]$DesiredInstanceCount, + [Parameter(Mandatory = $false)] + [Alias("server")] + [string]$ComputerName = "localhost" + ) + $loglead = (Get-LogLeadName); + Connect-AlkamiServiceFabricCluster -Hostname $ComputerName; + + # Locate the service fabric partition for the running application. + $serviceName = "$ServiceFabricApplicationName/svc"; + + # Set the service instance count. + try { + Update-ServiceFabricService -ServiceName $serviceName -InstanceCount $DesiredInstanceCount -Stateless -Force; + } + catch { + Write-Error "$loglead : Failed to set service count for application $ServiceFabricApplicationName. $_"; + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Start-AlkamiServiceFabricClusterUpgrade.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Start-AlkamiServiceFabricClusterUpgrade.ps1 new file mode 100644 index 0000000..715b414 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Start-AlkamiServiceFabricClusterUpgrade.ps1 @@ -0,0 +1,31 @@ +function Start-AlkamiServiceFabricClusterUpgrade { +<# +.SYNOPSIS + Updates the Service Fabric runtime version. + +.PARAMETER hostname + A single server from the cluster that needs to be upgraded. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost" + ) + + $loglead = Get-LogLeadName; + + Connect-AlkamiServiceFabricCluster -hostname $hostname; + + Write-Host "$loglead Updating Service Fabric runtime version."; + + [array]$versions = (Get-ServiceFabricRegisteredClusterCodeVersion | Select-Object -ExpandProperty CodeVersion | Sort-Object -Descending); + foreach($version in $versions) { + Write-Verbose "$loglead Found version $version"; + } + + $largestVersion = $versions[0]; + Write-Host "$loglead Selecting Service Fabric upgrade version $largestVersion. Starting upgrade."; + Start-ServiceFabricClusterUpgrade -Code -CodePackageVersion $largestVersion -Monitored -FailureAction Rollback; + + Write-Host "$loglead Cluster upgrade started. Progress can be monitored through the dashboard."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Test-AlkamiServiceFabricEnvironmentNodeTypeExists.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Test-AlkamiServiceFabricEnvironmentNodeTypeExists.ps1 new file mode 100644 index 0000000..9b08689 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Test-AlkamiServiceFabricEnvironmentNodeTypeExists.ps1 @@ -0,0 +1,39 @@ +function Test-AlkamiServiceFabricEnvironmentNodeTypeExists { +<# +.SYNOPSIS + Returns true if a Service Fabric worker node type exists for a given environment name. +.PARAMETER environmentName + The name of an orb environment. +.PARAMETER hostname + A single server hostname from the target Service Fabric cluster. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$environmentName, + [Parameter(Mandatory = $false)] + [string]$hostname = "localhost" + ) + + $loglead = (Get-LogLeadName); + + Connect-AlkamiServiceFabricCluster -hostname $hostname | Out-Null; + + # Query the cluster node configuration, and pull out the node type names. + $clusterConfigJson = Get-ServiceFabricClusterConfiguration | ConvertFrom-Json; + $nodeTypes = $clusterConfigJson.Properties.NodeTypes | Select-Object -ExpandProperty Name; + Write-Verbose "$loglead Found node types: $nodeTypes"; + + # Determine the name of the node type. + $workerName = (Format-AlkamiEnvironmentWorkerNodeType $environmentName); + Write-Verbose "$loglead Searching for NodeType '$workerName'"; + + # Determine if the node type exists on the cluster. + $nodeTypeExists = ($nodeTypes -contains $workerName); + if($nodeTypeExists) { + Write-Verbose "$loglead Found node type '$workerName'"; + } else { + Write-Verbose "$loglead Could not find node type '$workerName'"; + } + return $nodeTypeExists; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Unregister-AlkamiServiceFabricApplicationTypeOlderVersions.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Unregister-AlkamiServiceFabricApplicationTypeOlderVersions.ps1 new file mode 100644 index 0000000..936ca01 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Unregister-AlkamiServiceFabricApplicationTypeOlderVersions.ps1 @@ -0,0 +1,46 @@ +function Unregister-AlkamiServiceFabricApplicationTypeOlderVersions { + <# +.SYNOPSIS + Unregisters all microservice versions except the configured latest version(s). +.PARAMETER hostname + A single server Worker node hostname from the target Service Fabric cluster. +.PARAMETER ApplicationTypeRetentionCount + Number of versions to keep on the cluster. + #> + param ( + [Parameter(Mandatory = $false)] + [string]$Hostname = "localhost", + [Parameter(Mandatory = $false)] + $ApplicationTypeRetentionCount = 2 + ) + $logLead = (Get-LogLeadName); + Write-Host "$logLead : Removing all microservice versions except the $ApplicationTypeRetentionCount latest version(s)."; + + # Connect to the cluster and grab the deployed applications. + Connect-AlkamiServiceFabricCluster -hostname $Hostname | Out-Null; + + $serviceFabricApplications = Get-AlkamiServiceFabricApplications -ComputerName $Hostname + + foreach ($serviceFabricApplication in $serviceFabricApplications) { + $serviceFabricApplicationTypes = Get-ServiceFabricApplicationType -ApplicationTypeName $serviceFabricApplication.ServiceFabricApplicationTypeName + + if ($serviceFabricApplicationTypes.length -gt $ApplicationTypeRetentionCount ) { + $numberOfServiceFabricApplicationTypesToRemove = $serviceFabricApplicationTypes.Length - $ApplicationTypeRetentionCount + $serviceFabricApplicationTypes = Sort-Object -InputObject $serviceFabricApplicationTypes -Property $_.ApplicationTypeVersion -Descending + $serviceFabricApplicationTypeEligibleForRemovals = $serviceFabricApplicationTypes | Select-Object -First $numberOfServiceFabricApplicationTypesToRemove + + foreach ($serviceFabricApplicationTypeEligibleForRemoval in $serviceFabricApplicationTypeEligibleForRemovals) { + Write-Host "$logLead : Removing $($serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeName)|$($serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeVersion)" + try { + Unregister-ServiceFabricApplicationType -ApplicationTypeName $serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeName -ApplicationTypeVersion $serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeVersion -Force + } catch { + if ($_.Exception.ErrorCode -eq "ApplicationTypeInUse") { + Write-Warning "$loglead : $($_.Exception.Message) $($serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeName)|$($serviceFabricApplicationTypeEligibleForRemoval.ApplicationTypeVersion)" + } else { + Write-Error "$loglead : $($_.Exception.Message)" + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.Tests.ps1 new file mode 100644 index 0000000..912e568 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.Tests.ps1 @@ -0,0 +1,84 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Wait-AlkamiServiceFabricClusterHealthy" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Wait-AlkamiServiceFabricClusterHealthy.tests' } + Mock -CommandName Connect-AlkamiServiceFabricCluster -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Start-Sleep -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith {} + + Context "Parameter Validation" { + + It "Throws if Hostname is Null" { + { Wait-AlkamiServiceFabricClusterHealthy -Hostname $null } | Should -Throw + } + + It "Throws if Hostname is Empty" { + { Wait-AlkamiServiceFabricClusterHealthy -Hostname '' } | Should -Throw + } + + It "Throws if Sleep Interval is Zero" { + { Wait-AlkamiServiceFabricClusterHealthy -SleepIntervalSeconds 0 } | Should -Throw + } + + It "Throws if Sleep Interval is Too Large" { + { Wait-AlkamiServiceFabricClusterHealthy -SleepIntervalSeconds 180 } | Should -Throw + } + } + + Context "Error Handling" { + + Mock -CommandName Get-ServiceFabricClusterHealth -ModuleName $moduleForMock -MockWith { + return @{ 'AggregatedHealthState' = 'Test' } + } + + It "Writes Error if Operation Times Out" { + + Wait-AlkamiServiceFabricClusterHealthy -TimeoutMinutes 0 + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricClusterHealth -Times 1 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Timed out waiting for Service Fabric cluster to be healthy" } + } + } + + Context "Logic" { + + Mock -CommandName Get-ServiceFabricClusterHealth -ModuleName $moduleForMock -MockWith { + return @{ 'AggregatedHealthState' = 'Ok' } + } + + It "Uses Localhost Hostname By Default" { + + Wait-AlkamiServiceFabricClusterHealthy -TimeoutMinutes 0 + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -match 'localhost' } + } + + It "Uses Hostname Parameter if Provided" { + + Wait-AlkamiServiceFabricClusterHealthy -TimeoutMinutes 0 -Hostname 'Test' + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -match 'Test' } + } + + It "Returns Without Error if Cluster is Healthy" { + + Wait-AlkamiServiceFabricClusterHealthy -TimeoutMinutes 0 + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricClusterHealth -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.ps1 new file mode 100644 index 0000000..edea749 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricClusterHealthy.ps1 @@ -0,0 +1,59 @@ +function Wait-AlkamiServiceFabricClusterHealthy { + <# + .SYNOPSIS + Polls the status of a Service Fabric cluster containing the specified host until either a timeout + occurs or the cluster health status is 'Ok'. + + .PARAMETER Hostname + [string] The host name of any server in the Service Fabric cluster. If not provided, defaults to localhost. + + .PARAMETER TimeoutMinutes + [byte] The length of time in minutes to poll the cluster for a healthy status before declaring that an error + has occurred. Defaults to 30 minutes. + + .PARAMETER SleepIntervalSeconds + [byte] The length of time in seconds to pause between cluster status queries. Defaults to 15 seconds. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Hostname = 'localhost', + + [Parameter(Mandatory=$false)] + [byte]$TimeoutMinutes = 30, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 120)] + [byte]$SleepIntervalSeconds = 15 + ) + + $loglead = (Get-LogLeadName) + + Connect-AlkamiServiceFabricCluster -Hostname $Hostname + + Write-Host "$loglead : Waiting for Service Fabric cluster to be healthy." + + # Keep looping until the Service Fabric cluster is healthy or we timeout. + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + while($true) { + + $clusterHealth = Get-ServiceFabricClusterHealth + if ( $clusterHealth.AggregatedHealthState -eq 'Ok' ) { + + $stopwatch.Stop() + Write-Host "$logLead : Service Fabric cluster is healthy after $($stopwatch.Elapsed.ToString())." + break + + } elseif ( $stopwatch.Elapsed.TotalMinutes -gt $timeoutMinutes ) { + + $stopwatch.Stop() + Write-Error "$logLead : Timed out waiting for Service Fabric cluster to be healthy after $($stopwatch.Elapsed.ToString())." + break + + } else { + + Start-Sleep -Seconds $sleepIntervalSeconds + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.Tests.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.Tests.ps1 new file mode 100644 index 0000000..5b41901 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.Tests.ps1 @@ -0,0 +1,165 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Wait-AlkamiServiceFabricNodeStatus" { + + Mock -CommandName Get-LogLeadName -ModuleName $moduleForMock -MockWith { return 'Wait-AlkamiServiceFabricNodeStatus.tests' } + Mock -CommandName Connect-AlkamiServiceFabricCluster -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Start-Sleep -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Error -ModuleName $moduleForMock -MockWith { Write-Host $Message } + + Context "Parameter Validation" { + + It "Throws if Desired Status is Not Allowable Value" { + { Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Test' } | Should -Throw + } + + It "Throws if Hostname is Null" { + { Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -Hostname $null } | Should -Throw + } + + It "Throws if Hostname is Empty" { + { Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -Hostname '' } | Should -Throw + } + + It "Throws if Sleep Interval is Zero" { + { Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -SleepIntervalSeconds 0 } | Should -Throw + } + + It "Throws if Sleep Interval is Too Large" { + { Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -SleepIntervalSeconds 180 } | Should -Throw + } + } + + Context "Error Handling" { + + It "Writes Error if Node is Not Present in Cluster" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'NotTest' + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Could not retrieve the Service Fabric node" } + } + + It "Writes Error if Operation Times Out Due to Node Status" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test'; 'NodeStatus' = 'Test' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'Test' -SkipHealthCheck + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 2 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Timed out waiting for Service Fabric node" } + } + + It "Writes Error if Operation Times Out Due to Node Health" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test'; 'NodeStatus' = 'Up'; 'HealthState' = 'Test' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'Test' + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 2 -Exactly -Scope It + + Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Timed out waiting for Service Fabric node" } + } + } + + Context "Logic" { + + It "Uses Localhost Hostname By Default" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname; 'NodeStatus' = 'Up'; 'HealthState' = 'Ok' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + } + + It "Resolves Localhost Hostname to Computername for Status" { + + $testHostname = "$env:COMPUTERNAME" + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = $testHostname; 'NodeStatus' = 'Up'; 'HealthState' = 'Ok' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'localhost' } + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq $testHostname } + } + + It "Uses Hostname Parameter if Provided" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test'; 'NodeStatus' = 'Up'; 'HealthState' = 'Ok' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'Test' + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Hostname -eq 'Test' } + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 1 -Exactly -Scope It ` + -ParameterFilter { $NodeName -eq 'Test' } + } + + It "Respects Health State by Default" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test'; 'NodeStatus' = 'Up'; 'HealthState' = 'Ok' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'Test' + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 2 -Exactly -Scope It + } + + It "Ignores Health State if Parameter Provided" { + + Mock -CommandName Get-ServiceFabricNode -ModuleName $moduleForMock -MockWith { + return @{ 'NodeName' = 'Test'; 'NodeStatus' = 'Up'; 'HealthState' = 'Test' } + } + + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -TimeoutMinutes 0 -Hostname 'Test' -SkipHealthCheck + + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Connect-AlkamiServiceFabricCluster -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Get-ServiceFabricNode -Times 2 -Exactly -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.ps1 new file mode 100644 index 0000000..f7dd667 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricNodeStatus.ps1 @@ -0,0 +1,101 @@ +function Wait-AlkamiServiceFabricNodeStatus { + <# + .SYNOPSIS + Polls the status of a Service Fabric host until either a timeout occurs or the node reaches the + specified status. + + .PARAMETER DesiredStatus + [string] The target status of the Service Fabric node. Must be either 'Disabled' or 'Up'. + + .PARAMETER Hostname + [string] The host name of the server in the Service Fabric cluster to poll. If not provided, defaults to localhost. + + .PARAMETER TimeoutMinutes + [byte] The length of time in minutes to poll the cluster for a healthy status before declaring that an error + has occurred. Defaults to 30 minutes. + + .PARAMETER SleepIntervalSeconds + [byte] The length of time in seconds to pause between cluster status queries. Defaults to 15 seconds. + + .PARAMETER SkipHealthCheck + [switch] Flag indicating to skip validation that the node health is 'Ok' in addition to the node status. + + .EXAMPLE + Wait-AlkamiServiceFabricNodeStatus -DesiredStatus 'Up' -Hostname 'fab197478' + +WARNING: [Find-CertificateByName] Could not find certificate with Common Name staging-fabricadmin.alkamitech.com +[Connect-AlkamiServiceFabricCluster] Connected to Service Fabric cluster at endpoint fab197478:19000 +[Wait-AlkamiServiceFabricNodeStatus] : Waiting for Service Fabric node FAB197478 to have status of Up. +[Wait-AlkamiServiceFabricNodeStatus] : Service Fabric node FAB197478 has status of Up after 00:00:00.0607888. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Disabled', 'Up')] + [string]$DesiredStatus, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Hostname = 'localhost', + + [Parameter(Mandatory=$false)] + [byte]$TimeoutMinutes = 30, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 120)] + [byte]$SleepIntervalSeconds = 15, + + [Parameter(Mandatory=$false)] + [switch]$SkipHealthCheck + ) + + $loglead = (Get-LogLeadName) + + Connect-AlkamiServiceFabricCluster -Hostname $Hostname + + # Resolve the hostname into a service fabric node name (case-sensitive) + if ( $Hostname -eq 'localhost' ) { + + $actualHostname = "$env:COMPUTERNAME" + + } else { + + $actualHostname = $Hostname + } + + $curNode = Get-ServiceFabricNode | Where-Object { $_.NodeName -eq $actualHostname } | Select-Object -First 1 + if ( ! $curNode ) { + + Write-Error "$loglead : Could not retrieve the Service Fabric node for $actualHostname." + return + + } else { + + $curNodeName = $curNode.NodeName + } + + Write-Host "$loglead : Waiting for Service Fabric node $curNodeName to have status of $DesiredStatus." + + # Keep looping until the Service Fabric node has the desired status or we timeout. + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + while($true) { + + $curNode = Get-ServiceFabricNode -NodeName $curNodeName + if (( $curNode.NodeStatus -eq $DesiredStatus ) -and ( $SkipHealthCheck -or ( 'Ok' -eq $curNode.HealthState ))) { + + $stopwatch.Stop() + Write-Host "$logLead : Service Fabric node $curNodeName has status of $DesiredStatus after $($stopwatch.Elapsed.ToString())." + break + + } elseif ( $stopwatch.Elapsed.TotalMinutes -gt $timeoutMinutes ) { + + $stopwatch.Stop() + Write-Error "$logLead : Timed out waiting for Service Fabric node $curNodeName to be in state $DesiredStatus after $($stopwatch.Elapsed.ToString())." + break + + } else { + + Start-Sleep -Seconds $sleepIntervalSeconds + } + } +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricUpgrades.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricUpgrades.ps1 new file mode 100644 index 0000000..4690f0e --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/Public/Wait-AlkamiServiceFabricUpgrades.ps1 @@ -0,0 +1,82 @@ +function Wait-AlkamiServiceFabricUpgrades { +<# +.SYNOPSIS + Waits until the specified package deployments are complete in Service Fabric. + Throws an error if any of the packages failed to deploy and were rolled back. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [object[]]$packages, + [Parameter(Mandatory=$true)] + [string]$environmentName, + [Parameter(Mandatory=$false)] + [int]$timeoutMinutes = 30, + [Parameter(Mandatory=$false)] + [int]$sleepIntervalSeconds = 15 + ) + + $packages = $packages | Where-Object { ![string]::IsNullOrWhiteSpace($_.Version) }; + if(($null -eq $packages) -or ($packages.Count -eq 0)) { + Write-Host "$loglead : No packages were specified. Returning."; + return; + } + + $loglead = (Get-LogLeadName); + + Write-Host "$loglead : Waiting for upgrades of $($packages.Count) packages to complete."; + + $completedStates = @("RollingForwardCompleted", "RollingBackCompleted", "Failed"); + $successState = $completedStates[0]; + + # Keep looping until all of the requested deployments are finished, or the deployment lasts longer than the timeout period. + $stopwatch = [system.diagnostics.stopwatch]::StartNew(); + while($true) { + + # Grab all of the upgrade status objects for each of the packages from Service Fabric. + $packageUpgradeStates = @(); + foreach($package in $packages) { + + # Look for the upgrade of the service. + Write-Verbose "$loglead : Getting upgrade status for package $($package.Name)"; + if($package.IsReliableService) { + $applicationName = Format-AlkamiServiceFabricApplicationName -name $package.ApplicationTypeName -version $package.Version; + } else { + $applicationName = Format-AlkamiServiceFabricApplicationName -name $package.Name -version $package.Version -environmentName $environmentName; + } + $applicationName = "fabric:/$applicationName"; + + $upgradeStatus = Get-ServiceFabricApplicationUpgrade -ApplicationName $applicationName; + $packageUpgradeStates += $upgradeStatus; + } + + # If there are still deployments running sleep, and the loop goes on + $incompleteDeployments = $packageUpgradeStates | Where-Object { $completedStates -notcontains $_.UpgradeState }; + if($stopwatch.Elapsed.TotalMinutes -gt $timeoutMinutes) { + Write-Error "$loglead : Service Fabric Upgrades have exceeded timeout window of $timeoutMinutes minutes. Exiting."; + break; + } elseif($null -ne $incompleteDeployments) { + Write-Host "$loglead : Service Fabric Upgrades are still in progress for $($incompleteDeployments.Count) packages. Waiting $($sleepIntervalSeconds)s."; + Start-Sleep -Seconds $sleepIntervalSeconds; + continue; + } + + # If we've made it here the deployments are complete. + # Verify that all of the deployments finished successfully, and are the correct versions. + $failure = $false; + foreach($state in $packageUpgradeStates) { + if($state.UpgradeState -ne $successState) { + Write-Error "$loglead : Upgrade of package $($state.ApplicationTypeName) was unsuccessful. Status: $($state.UpgradeState)" -ErrorAction Continue; + $failure = $true; + } + } + if($failure) { + Write-Error "$loglead : One or more Service Fabric package upgrades were not successful. Investigate!"; + } + + # All of the deployments are complete. Break the loop. + break; + } + + Write-Host "$loglead : Package upgrades are complete."; +} diff --git a/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/AlkamiDevClusterConfig.json b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/AlkamiDevClusterConfig.json new file mode 100644 index 0000000..5364467 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/AlkamiDevClusterConfig.json @@ -0,0 +1,88 @@ +{ + "name": "DevCluster", + "clusterConfigurationVersion": "1.0.0", + "apiVersion": "10-2017", + "nodes": [ + { + "nodeName": "_Node_0", + "iPAddress": "localhost", + "nodeTypeRef": "NodeType0", + "faultDomain": "fd:/0", + "upgradeDomain": "0" + } + ], + "properties": { + "diagnosticsStore": + { + "metadata": "Please replace the diagnostics file share with an actual file share accessible from all cluster machines. For example, \\\\localhost\\DiagnosticsStore.", + "dataDeletionAgeInDays": "21", + "storeType": "FileShare", + "connectionstring": "C:\\SFDevCluster\\Data" + }, + "security": { + "metadata": "The Credential type X509 indicates this is cluster is secured using X509 Certificates. The thumbprint format is - d5 ec 42 3b 79 cb e5 07 fd 83 59 3c 56 b9 d5 31 24 25 42 64.", + "CertificateInformation": { + "ReverseProxyCertificateCommonNames": { + "CommonNames": [ + { + "CertificateCommonName": "*.dev.alkamitech.com" + } + ], + "X509StoreName": "My" + } + } + }, + "nodeTypes": [ + { + "name": "NodeType0", + "clientConnectionEndpointPort": "19000", + "clusterConnectionEndpointPort": "19002", + "leaseDriverEndpointPort": "19001", + "serviceConnectionEndpointPort": "19006", + "httpGatewayEndpointPort": "19080", + "reverseProxyEndpointPort": "19081", + "applicationPorts": { + "startPort": "30001", + "endPort": "31000" + }, + "isPrimary": true + } + ], + "fabricSettings": [ + { + "name": "Setup", + "parameters": [ + { + "name": "FabricDataRoot", + "value": "C:\\SFDevCluster\\Data" + }, + { + "name": "FabricLogRoot", + "value": "C:\\SFDevCluster\\Log" + }, + { + "name": "SkipFirewallConfiguration", + "value": "true" + } + ] + }, + { + "name": "UpgradeOrchestrationService", + "parameters": [ + { + "name": "MinReplicaSetSize", + "value": "0" + }, + { + "name": "TargetReplicaSetSize", + "value": "0" + } + ] + } + ], + "addOnFeatures": [ + "DnsService", + "EventStoreService" + ] + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ApplicationManifest.xml b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ApplicationManifest.xml new file mode 100644 index 0000000..5bd6ff3 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ApplicationManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ClusterConfig.json b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ClusterConfig.json new file mode 100644 index 0000000..a1d814c --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ClusterConfig.json @@ -0,0 +1,152 @@ +{ + "name": "REPLACEME", + "clusterConfigurationVersion": "1.0.0", + "apiVersion": "10-2017", + "nodes": [ + { + "nodeName": "REPLACEME", + "iPAddress": "REPLACEME", + "nodeTypeRef": "SeedNode", + "faultDomain": "fd:/REPLACEME", + "upgradeDomain": "REPLACEME" + } + ], + "properties": { + "diagnosticsStore": + { + "metadata": "Please replace the diagnostics file share with an actual file share accessible from all cluster machines. For example, \\\\machine1\\DiagnosticsStore.", + "dataDeletionAgeInDays": "21", + "storeType": "FileShare", + "connectionstring": "C:\\ProgramData\\SF\\DiagnosticStore" + }, + "security": { + "ClusterCredentialType": "Windows", + "ServerCredentialType": "X509", + "WindowsIdentities": { + "ClusterIdentity" : "REPLACEME - domain\\GMSA Group Name" + }, + "CertificateInformation": { + "ServerCertificateCommonNames": { + "CommonNames": [ + { + "CertificateCommonName": "REPLACEME - env-fabricadmin.alkamitech.com" + } + ], + "X509StoreName": "My" + }, + "ClientCertificateCommonNames": [ + { + "CertificateCommonName": "REPLACEME - env-fabricadmin.alkamitech.com", + "CertificateIssuerThumbprint": "REPLACEME - thumbprint of the issuing certificate", + "IsAdmin": true + }, + { + "CertificateCommonName": "REPLACEME - env-fabric.alkamitech.com", + "CertificateIssuerThumbprint": "REPLACEME - thumbprint of the issuing certificate", + "IsAdmin": false + } + ] + } + }, + "nodeTypes": [ + { + "name": "SeedNode", + "clientConnectionEndpointPort": "19000", + "clusterConnectionEndpointPort": "19001", + "leaseDriverEndpointPort": "19002", + "serviceConnectionEndpointPort": "19003", + "httpGatewayEndpointPort": "19080", + "reverseProxyEndpointPort": "19081", + "applicationPorts": { + "startPort": "20001", + "endPort": "20031" + }, + "isPrimary": true + } + ], + "AddonFeatures": [ + "ResourceMonitorService" + ], + "fabricSettings": [ + { + "name": "Setup", + "parameters": [ + { + "name": "FabricDataRoot", + "value": "C:\\ProgramData\\SF" + }, + { + "name": "FabricLogRoot", + "value": "C:\\ProgramData\\SF\\Log" + }, + { + "Name": "SkipFirewallConfiguration", + "Value": "true" + } + ] + }, + { + "Name": "ClusterManager", + "Parameters": [ + { + "Name": "EnableDefaultServicesUpgrade", + "Value": "true" + } + ] + }, + { + "Name": "Security", + "Parameters": [ + { + "Name": "CrlCheckingFlag", + "Value": "4" + } + ] + }, + { + "Name": "Federation", + "Parameters": [ + { + "Name": "X509CertChainFlags", + "Value": "4" + } + ] + }, + { + "Name": "PlacementAndLoadBalancing", + "Parameters": [ + { + "Name": "UseMoveCostReports", + "Value": "true" + } + ] + }, + { + "Name": "PlacementAndLoadBalancing", + "Parameters": [ + { + "Name": "PLBRefreshGap", + "Value": "0.10" + }, + { + "Name": "MinPlacementInterval", + "Value": "1.0" + }, + { + "Name": "MinConstraintCheckInterval", + "Value": "1.0" + }, + { + "Name": "MinLoadBalancingInterval", + "Value": "1.0" + }, + { + "Name": "GlobalMovementThrottleThresholdPercentage", + "Value": "10.0" + } + ] + } + ], + "FabricClusterAutoupgradeEnabled": false + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ServiceManifest.xml b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ServiceManifest.xml new file mode 100644 index 0000000..dfa4151 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/ServiceManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + REPLACEME - EXE Name (Alkami.*.exe) + + CodeBase + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/Settings.xml b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/Settings.xml new file mode 100644 index 0000000..6c64337 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/ServiceFabricConfigTemplates/Settings.xml @@ -0,0 +1,7 @@ + + + +
+ +
+
diff --git a/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..8b0d3a2 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyInstall.ps1 @@ -0,0 +1,42 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + + # Adding Service Fabric bin directory to path. + $sfBinPath = "C:\Program Files\Microsoft Service Fabric\bin\Fabric\Fabric.Code"; + Write-Host "Adding Service Fabric bin directory to Path: $sfBinPath"; + Add-DirectoryToPath $sfBinPath; + $env:path += ";$sfBinPath"; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.ServiceFabric/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.nuspec b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.nuspec new file mode 100644 index 0000000..4a713d7 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.nuspec @@ -0,0 +1,33 @@ + + + + Alkami.PowerShell.Services + $version$ + Alkami Platform Modules - PowerShell - Services + Alkami Technologies + Alkami Technologies + https://extranet.alkamitech.com/display/ORB/Alkami.PowerShell.Services + https://www.alkami.com/files/alkamilogo75x75.png + http://alkami.com/files/orblicense.html + false + Installs the Alkami Services module for use with PowerShell. + + PowerShell + Copyright (c) 2018 Alkami Technologies + + + + + + + + + + + + + + + + + diff --git a/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.psd1 b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.psd1 new file mode 100644 index 0000000..adde4d0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.psd1 @@ -0,0 +1,13 @@ +@{ + RootModule = 'Alkami.PowerShell.Services.psm1' + ModuleVersion = '3.23.2' + GUID = 'a1d890c2-3b78-41fe-8c2c-59d93f310cb9' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.' + Description = 'A set of functions for managing Windows Services.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common', 'Alkami.PowerShell.AD', 'Alkami.PowerShell.Configuration' + FunctionsToExport = 'Clear-GMSAPasswords','Disable-Nag','Disable-Radium','Disable-Service','Enable-Nag','Enable-Radium','Enable-Service','Get-AlkamiServices','Get-AppTierServices','Get-ChocolateyServices','Get-ChocolateyServicesToStart','Get-FileBeatsService','Get-FileBeatsServicePaths','Get-NagTriggersCount','Get-ProcessFromService','Get-ServiceByChocoName','Get-ServiceInfoByCIMFragment','Get-ServiceNamesByFragment','Get-ServiceStartupFailuresFromEventLog','Get-ServicesToStart','Get-ServicesToStop','Get-WindowsServiceApplicationName','Get-WindowsServiceApplicationPath','Grant-UserLogonAsServiceRights','Grant-UserStartStopRightsToService','Install-AlkamiService','Install-LegacyMicroservice','Invoke-SCExe','Invoke-ServiceInstall','Invoke-TopshelfPath','New-AppTierWindowsServices','Ping-AlkamiServices','Restart-Nag','Set-ChocolateyPackageNewRelicState','Set-DotNetCoreProfiling','Set-ServiceAccountManagedState','Set-ServiceRecoveryOneRestart','Set-WindowsServiceExecutionAccount','Start-AlkamiService','Start-DependentServices','Start-FileBeatsService','Start-ServicesChocolateyOnly','Start-ServicesInParallel','Start-ServicesOnly','Stop-AlkamiService','Stop-ServicesChocolateyOnly','Stop-ServicesInParallel','Stop-ServicesOnly','Test-AreCriticalNagJobsRunning','Test-IsNagRunning','Test-IsNagServer','Test-NagService','Test-NagTriggers','Test-RadiumMetaData','Test-RadiumService','Uninstall-AlkamiService','Uninstall-LegacyMicroservice' + AliasesToExport = 'Create-AppTierWindowsServices' +} diff --git a/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.pssproj b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.pssproj new file mode 100644 index 0000000..9e59c38 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Alkami.PowerShell.Services.pssproj @@ -0,0 +1,99 @@ + + + Debug + 2.0 + {ccd156f6-c5d9-4a68-a32e-1ad32cca4d6d} + Exe + MyApplication + MyApplication + Alkami.PowerShell.Services + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.PowerShell.Services") + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/AlkamiManifest.xml b/Modules/Alkami.PowerShell.Services/AlkamiManifest.xml new file mode 100644 index 0000000..8f2b747 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Alkami.PowerShell.Services + SREModule + + + Production + + diff --git a/Modules/Alkami.PowerShell.Services/Private/Invoke-DeleteLegacyMicroserviceFromServiceCandidate.ps1 b/Modules/Alkami.PowerShell.Services/Private/Invoke-DeleteLegacyMicroserviceFromServiceCandidate.ps1 new file mode 100644 index 0000000..ce19471 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Private/Invoke-DeleteLegacyMicroserviceFromServiceCandidate.ps1 @@ -0,0 +1,55 @@ +function Invoke-DeleteLegacyMicroserviceFromServiceCandidate { +<# +.SYNOPSIS + Private method to system unregister a legacy service. Called from multiple places. + +.DESCRIPTION + Unregisters the Windows Service at a core location + +.PARAMETER ServiceCandidate + [PSObject] A service candidate (needs properties Path and Name) + +.EXAMPLE +# Get a list of candidates for a fragment +$ServiceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $fragment) +foreach($ServiceCandidate in $ServiceCandidates) { + Invoke-DeleteLegacyMicroserviceFromServiceCandidate $ServiceCandidate +} +#> + param ( + [PSObject]$ServiceCandidate + ) + + $logLead = (Get-LogLeadName) + + $ServiceCandidatePath = $ServiceCandidate.ExePath + $ServiceCandidateName = $ServiceCandidate.Name + + Write-Host "$logLead : Trying to remove service [$($ServiceCandidateName)] from [$ServiceCandidatePath]" + Stop-AlkamiService -ServiceName $ServiceCandidateName + + if (Test-Path $serviceCandidatePath) { + # Since we are getting the modified ExePath from the CimFragment in our own code, we may need + # to switch the path out if those params from TopShelf are problematic? idk + Invoke-TopshelfPath $ServiceCandidatePath @("uninstall") + } else { + Write-Host "$logLead : Chocolatey path not found at [$serviceCandidatePath]. Will attempt to delete with sc.exe" + } + + if ($null -ne (Get-Service -Name $ServiceCandidateName -ErrorAction Ignore)) { + # the topshelf installer did not uninstall, let's fall back to sc.exe + + Write-Verbose "$logLead : Removing [$ServiceCandidateName] via sc.exe" + Invoke-SCExe @("delete",$ServiceCandidateName) + + # Do the least thing possible + if ($LASTEXITCODE -eq 1072) { Start-Sleep -Seconds 30 } + # Give Windows time to breathe. This might could be shorter, who knows. 150 is a magic number from thin air. + Start-Sleep -Milliseconds 150 + + if ($null -ne (Get-Service -Name $ServiceCandidateName -ErrorAction Ignore)) { + # How did this happen? sc.exe is a hot-knife. + throw "$logLead : Tried to uninstall [$ServiceCandidateName] from [$ServiceCandidatePath] but it seems to still be present" + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Private/VariableDeclarations.ps1 b/Modules/Alkami.PowerShell.Services/Private/VariableDeclarations.ps1 new file mode 100644 index 0000000..bf29fc2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Private/VariableDeclarations.ps1 @@ -0,0 +1,14 @@ +try +{ + Add-Type -Path (Get-ChildItem -Path "C:\Windows\assembly\" -Include "Microsoft.Web.Administration.dll" -Recurse).FullName +} +catch +{ + try { + # Just in case we have IIS Express and IIS loaded (dev machines) + [System.Reflection.Assembly]::LoadFile("C:\Windows\system32\inetsrv\Microsoft.Web.Administration.dll") + } catch { + # Do nothing in case this is a brand new server + Write-Warning "[Alkami.PowerShell.Services] : Unable to Load Assembly Microsoft.Web.Administration. Some functions may not work as expected." + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.ps1 b/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.ps1 new file mode 100644 index 0000000..041344b --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.ps1 @@ -0,0 +1,107 @@ +function Clear-GMSAPasswords { + +<# +.SYNOPSIS + Sets an empty password on all Chocolatey services running as GMSA Accounts. Accepts an optional string array as a filter parameter + +.DESCRIPTION + Due to bugs in the way our microservice services are created, GMSA Password rotations may result in services failing to start with logon errors (see: SDK-773) + This function is occasionally run to clear out the passwords and allow them to start. All Windows services running out of the Chocolatey folder and running as + GMSA accounts will be acted on (including NAG and Radium), unless a filter list of full Service Names is specified + +.PARAMETER serviceFilter + [string[]] Optional Array of Service Names (not display names) to execute against + +.EXAMPLE + Clear-GMSAPasswords + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 3 chocolatey services. +[Clear-GMSAPasswords] : Clearing GMSA Password for Service Alkami.Services.Subscriptions.Host running as user FH\stage.micro$ +[SC] ChangeServiceConfig SUCCESS +[Clear-GMSAPasswords] : Clearing GMSA Password for Service Alkami.MicroServices.Broker.Host running as user FH\stage.micro$ +[SC] ChangeServiceConfig SUCCESS +[Clear-GMSAPasswords] : Clearing GMSA Password for Service Alkami.MicroServices.Features.Beacon.Host running as user FH\stage.micro$ +[SC] ChangeServiceConfig SUCCESS +[Clear-GMSAPasswords] : Cleared 3 GMSA Service Passwords + +.EXAMPLE + Clear-GMSAPasswords @("Alkami.Services.Subscriptions.Host", "Alkami.MicroServices.Broker.Host") -Verbose + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path. +[Get-ChocolateyServices] : Found 3 chocolatey services. +[Clear-GMSAPasswords] : Filtering for Services: +Alkami.Services.Subscriptions.Host +Alkami.MicroServices.Broker.Host +[Clear-GMSAPasswords] : Clearing GMSA Password for Service Alkami.Services.Subscriptions.Host running as user FH\stage.micro$ +[SC] ChangeServiceConfig SUCCESS +[Clear-GMSAPasswords] : Clearing GMSA Password for Service Alkami.MicroServices.Broker.Host running as user FH\stage.micro$ +[SC] ChangeServiceConfig SUCCESS +VERBOSE: [Clear-GMSAPasswords] : Skipping Service Alkami.MicroServices.Features.Beacon.Host as it is not in the Filter List +[Clear-GMSAPasswords] : Cleared 2 GMSA Service Passwords + +#> + + param( + [CmdletBinding()] + [Parameter(Mandatory=$false)] + [string[]]$serviceFilter + ) + + $logLead = Get-LogLeadName + $filterParamSpecified = !(Test-IsCollectionNullOrEmpty $serviceFilter) + + [array]$services = Get-ChocolateyServices + + [array]$nagAndRadium = Get-AlkamiServices | Where-Object {($_.Name -match "Nag|Radium")} + if (Test-IsCollectionNullOrEmpty $nagAndRadium) { + Write-Verbose "$logLead : No Nag/Radium services running on host." + } else { + $services += $nagAndRadium + } + + if (Test-IsCollectionNullOrEmpty $services) { + + Write-Warning "$logLead : Found no Services! Execution cannot continue"; + return; + } + + if ($filterParamSpecified) { + + Write-Host "$logLead : Filtering for Services:" + Write-Host $serviceFilter -Separator `n + } + + $clearedCount = 0 + foreach ($serviceName in ($services | Select-Object -ExpandProperty Name -Unique)) { + + if ($filterParamSpecified -and (!($serviceFilter -icontains $serviceName))) { + + Write-Verbose "$logLead : Skipping Service $serviceName as it is not in the Filter List" + continue; + } + + $userName = Get-WindowsServiceUser $serviceName + + if (!($userName.EndsWith("$"))) { + + Write-Warning "$logLead : Skipping Service $serviceName as it is Not Running as a GMSA Account" + continue; + } + + Write-Host "$logLead : Clearing GMSA Password for Service $serviceName running as user $userName" + $params = @("config", $serviceName, "obj=$userName") + Invoke-SCExe $params + $clearedCount++ + + Write-Host "$logLead : Setting $serviceName to managed by LSA" + Set-ServiceAccountManagedState -ServiceName $serviceName + } + + if ($filterParamSpecified -and $clearedCount -eq 0 -and $services.Count -gt 0) { + + Write-Warning "$logLead : Found no matching services based on the supplied parameters" + } + + Write-Host "$logLead : Updated $clearedCount GMSA Services" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.tests.ps1 new file mode 100644 index 0000000..d1c1425 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Clear-GMSAPasswords.tests.ps1 @@ -0,0 +1,228 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Clear-GMSAPasswords" { + + Context "Error Handling" { + + It "Writes a Warning and Returns if No Services Found" { + + Mock -CommandName Get-ChocolateyServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-WindowsServiceUser -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + + Clear-GMSAPasswords + + # Assert the Warning is Written + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "Found no Services" } + + # Assert No Calls to Invoke-SCExe are made when no services are found + Assert-MockCalled -CommandName Invoke-SCExe -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Writes a Warning and Skips Services Not Running as GMSA Accounts" { + + Mock -CommandName Get-ChocolateyServices -MockWith { return @( New-Object PSObject -Property @{ Name="Super.Fake.Awesome.Service"; } ) } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @( New-Object PSObject -Property @{ Name="Other.Fake.Awesome.Nag.Service"; } ) } -ModuleName $moduleForMock + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User" } -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + + Clear-GMSAPasswords + + # Assert that a warning is written for both Choco and Legacy services + Assert-MockCalled -CommandName Write-Warning -Times 2 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "Skipping Service" } + + # Assert No Calls to Invoke-SCExe are made when no services are found running under gMSA context + Assert-MockCalled -CommandName Invoke-SCExe -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Writes a Warning if Services Were Found But None Match the Filter List Parameter" { + + Mock -CommandName Get-ChocolateyServices -MockWith { return @( New-Object PSObject -Property @{ Name="Super.Fake.Awesome.Service"; } ) } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @( New-Object PSObject -Property @{ Name="Super.Fake.Radium.Service"; } ) } -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + + Clear-GMSAPasswords "NonExistent Service!" + + # Assert that a warning is written because no services are found matching the supplied filter + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "Found no matching services" } + + # Assert No Calls to Invoke-SCExe are made when no matching services are found + Assert-MockCalled -CommandName Invoke-SCExe -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } + + Context "External Calls" { + + It "Calls Set-ServiceAccountManagedState for Each Service When No Service Filter Supplied" { + + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User$" } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return @( + (New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + }), + (New-Object PSObject -Property @{ + Name="NotSoSuper.Fake.Sucky.Service"; + }) + ) + } + + Clear-GMSAPasswords + + # Make sure Set-ServiceAccountManagedState was called for each service + Assert-MockCalled -CommandName Set-ServiceAccountManagedState -Times 2 -Exactly -Scope It + + # Make sure Set-ServiceAccountManagedState was called with the expected service name filters + Assert-MockCalled -CommandName Set-ServiceAccountManagedState -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $ServiceName -match "Super.Fake.Awesome.Service" } + Assert-MockCalled -CommandName Set-ServiceAccountManagedState -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $ServiceName -match "NotSoSuper.Fake.Sucky.Service" } + } + + It "Correctly Filters Set-ServiceAccountManagedState Based on a Supplied Service Name String" { + + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User$" } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return @( + (New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + }), + (New-Object PSObject -Property @{ + Name="NotSoSuper.Fake.Sucky.Service"; + }) + ) + } + + Clear-GMSAPasswords "Super.Fake.Awesome.Service" + + # Make sure Set-ServiceAccountManagedState was only called once + Assert-MockCalled -CommandName Set-ServiceAccountManagedState -Times 1 -Exactly -Scope It + + # Make sure Set-ServiceAccountManagedState was called with the expected service name filter + Assert-MockCalled -CommandName Set-ServiceAccountManagedState -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $ServiceName -match "Super.Fake.Awesome.Service" } + } + } + + Context "Parameter Handling" { + + It "Correctly Filters Based on a Supplied Service Name String" { + + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User$" } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return @( + (New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + }), + (New-Object PSObject -Property @{ + Name="NotSoSuper.Fake.Sucky.Service"; + }) + ) + } + + Clear-GMSAPasswords "Super.Fake.Awesome.Service" -Verbose + + # Assert that Invoke-SCExe is only called once due to the supplied service name filter + Assert-MockCalled -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + + # Make sure Invoke-SCExe was called with the expected service name filter + Assert-MockCalled -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Arguments -contains "Super.Fake.Awesome.Service" } + + # Assert that we write a verbose message indicating the other service was skipped + Assert-MockCalled -CommandName Write-Verbose -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "Skipping Service NotSoSuper.Fake.Sucky.Service" } + } + + It "Correctly Filters Based on a Service Name Array" { + + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User$" } -ModuleName $moduleForMock + Mock -CommandName Get-AlkamiServices -MockWith { return @() } -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + return @( + (New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + }), + (New-Object PSObject -Property @{ + Name="Yet.Another.Awesome.Service"; + }), + (New-Object PSObject -Property @{ + Name="NotSoSuper.Fake.Sucky.Service"; + }) + ) + } + + Clear-GMSAPasswords @("Super.Fake.Awesome.Service","Yet.Another.Awesome.Service") -Verbose + + # Assert that Invoke-SCExe was called twice for both services in the array param + Assert-MockCalled -CommandName Invoke-SCExe -Times 2 -Exactly -Scope It -ModuleName $moduleForMock + + # Make sure Invoke-SCExe was called with the expected service name filter + Assert-MockCalled -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Arguments -contains "Super.Fake.Awesome.Service" } + Assert-MockCalled -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Arguments -contains "Yet.Another.Awesome.Service" } + + # Assert that we write a verbose message indicating the other service was skipped + Assert-MockCalled -CommandName Write-Verbose -Times 1 -Exactly -Scope It -ModuleName $moduleForMock -ParameterFilter { $Message -match "Skipping Service NotSoSuper.Fake.Sucky.Service" } + } + + It "Only Acts on Unique Service Names Once" { + + Mock -CommandName Get-WindowsServiceUser -MockWith { return "FH\fake.User$" } -ModuleName $moduleForMock + Mock -CommandName Invoke-SCExe -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Set-ServiceAccountManagedState -MockWith {} -ModuleName $moduleForMock + + Mock -CommandName Get-AlkamiServices -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + } + } + + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + Name="Super.Fake.Awesome.Service"; + } + } + + Clear-GMSAPasswords @("Super.Fake.Awesome.Service") + + # Assert that Invoke-SCExe is only called once though the same service name was returned by both + # Get-AlkamiServices and Get-ChocolateyServices + Assert-MockCalled -CommandName Invoke-SCExe -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.ps1 new file mode 100644 index 0000000..2937e42 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.ps1 @@ -0,0 +1,38 @@ +function Disable-Nag { +<# +.SYNOPSIS + Break Nag By Disabling the Service and Adding Bogus Values to the Config + +.PARAMETER nagPath + The path where Nag is installed. Defaults to (Get-OrbPath)\Nag +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [string]$nagPath + ) + + $logLead = (Get-LogLeadName); + + if([string]::IsNullOrEmpty($nagPath)) { + $nagPath = (Join-Path(Get-OrbPath) "\Nag") + } + + Write-Verbose "$logLead : Using Nag Path $nagPath" + + $nagService = Get-Service "Alkami Nag Service" -ErrorAction SilentlyContinue + + if ($null -eq $nagService) { + Write-Host "$logLead : The Nag Service is not installed on this machine" + } else { + Disable-Service $nagService + Stop-AlkamiService $nagService.Name + } + + # ToDo: When dev changes the app setting key in ORB we'll remove the term WhiteList from this function + # Until then, we will match the development code base to avoid any confusion + # See: SRE-14133 + $nagConfig = (Join-Path $nagPath "Alkami.App.Nag.Host.Service.exe.config") + Set-AppSetting -key 'TenantFIWhitelistRegex' -value 'DISABLE ALL TENANTS' -filePath $nagConfig + Set-AppSetting -key 'JobNameWhitelistRegex' -value 'DISABLE ALL JOBS' -filePath $nagConfig +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.tests.ps1 new file mode 100644 index 0000000..ebd679e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Nag.tests.ps1 @@ -0,0 +1,93 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Disable-Nag + +Describe "Disable-Nag" { + + # Temp file to write content to + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = $tempFile.Split(".") | Select-Object -First 1 + + New-Item -ItemType Directory $tempPath -ErrorAction SilentlyContinue | Out-Null + Write-Warning ("Using temp path: $tempPath for tests") + + $tempConfig = (Join-Path $tempPath "Alkami.App.Nag.Host.Service.exe.config") + + Context "Output File Validation" { + + Mock Get-Service { return $null } + + It "Updates the Nag Configuration With Fake Tenant and Job Whitelist Values" { + #Arrange + @" + + + + + + + +"@ | Out-File $tempConfig -Force + #Act + { Disable-Nag $tempPath } | Should -Not -Throw + #Assert + [xml]$outputXml = GC $tempConfig + $outputXml.SelectSingleNode("//appSettings/add[@key='TenantFIWhitelistRegex']").Value | Should -Be "DISABLE ALL TENANTS" + $outputXml.SelectSingleNode("//appSettings/add[@key='JobNameWhitelistRegex']").Value | Should -Be "DISABLE ALL JOBS" + } + + It "Retains Fake Tenant and Job Whitelist Values If They Exist" { + #Arrange + @" + + + + + + + +"@ | Out-File $tempConfig -Force + #Act + { Disable-Nag $tempPath } | Should -Not -Throw + #Assert + [xml]$outputXml = GC $tempConfig + $outputXml.SelectSingleNode("//appSettings/add[@key='TenantFIWhitelistRegex']").Value | Should -Be "DISABLE ALL TENANTS" + $outputXml.SelectSingleNode("//appSettings/add[@key='JobNameWhitelistRegex']").Value | Should -Be "DISABLE ALL JOBS" + } + } + + Context "Windows Service Validation" { + + $testServiceName = "Alkami Nag Service" + + It "Disables the Nag Service if It Is Installed" { + + try { + #Arrange + New-Service -Name $testServiceName -BinaryPathName "FakePath" + $testSvc = Get-Service -Name $testServiceName + + if ($testSvc.StartType -eq "Disabled") { + throw "Service Already Disabled -- Invalid Test Setup Performed" + } + + #Act + { Disable-Nag $tempPath } | Should -Not -Throw + #Assert + (Get-Service $testServiceName).StartType -eq "Disabled" | Should -Be $true + } finally { + if ($null -ne (Get-Service $testServiceName -ErrorAction SilentlyContinue)) { + (Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $testServiceName)) | Invoke-CIMMethod -MethodName Delete + } + } + } + } +} + +#endregion Disable-Nag \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.Tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.Tests.ps1 new file mode 100644 index 0000000..54a5511 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.Tests.ps1 @@ -0,0 +1,37 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Disable-Radium" -Tag 'Unit' { + Mock -CommandName "Disable-Service" -MockWith {} -ModuleName $moduleForMock + <# + Pester `Mock -MockWith` is ... weird. + It lets you use parameter variable names from *inside* the scope of the namespace you're + mocking the command call *from*. + So $sName below is not evaluated until the call to Stop-AlkamiService from *inside* of + Alkami.PowerShell.Services happens. At which point, $sName has a value + https://github.com/pester/Pester/wiki/Mock - where they describe the `Mock-With` parameter. + #> + Mock -CommandName "Stop-AlkamiService" -MockWith { return $sName } -ModuleName $moduleForMock + Mock -CommandName "Get-Service" { + New-MockObject -Type System.ServiceProcess.ServiceController + } -ModuleName $moduleForMock + It "calls Disable-Service with ServiceObject" { + Disable-Radium + Assert-MockCalled -ModuleName $moduleForMock -CommandName Disable-Service -ParameterFilter { $service } -Scope It -Exactly 1 + } + It "calls Stop-AlkamiService with 'Alkami Radium Scheduler Service'" { + Disable-Radium + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -ParameterFilter { $sName -eq 'Alkami Radium Scheduler Service' } -Scope It -Exactly 1 + } + It "does nothing if Radium not found" { + Mock -CommandName "Get-Service" {return $null} -ModuleName $moduleForMock + Disable-Radium + Assert-MockCalled -ModuleName $moduleForMock -CommandName Disable-Service -ParameterFilter { $service } -Scope It -Exactly 0 + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -ParameterFilter { $sName -eq 'Alkami Radium Scheduler Service' } -Scope It -Exactly 0 + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.ps1 new file mode 100644 index 0000000..84ad991 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Radium.ps1 @@ -0,0 +1,22 @@ +function Disable-Radium { +<# +.SYNOPSIS + Disables and Stops Radium Scheduler Service + +#> + param( + + ) + + $radiumServiceName = 'Alkami Radium Scheduler Service' + $logLead = (Get-LogleadName); + Write-Verbose "$logLead : Disabling Radium..." + $radiumService = Get-Service -Name $radiumServiceName + if ($null -eq $radiumService) { + Write-Warning "$logLead : Radium service |$radiumServiceName| is not installed on this host." + return + } + Disable-Service -ServiceObject $radiumService + Stop-AlkamiService -ServiceName $radiumServiceName + +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Service.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Service.ps1 new file mode 100644 index 0000000..a9976ee --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Service.ps1 @@ -0,0 +1,37 @@ +function Disable-Service { +<# +.SYNOPSIS + Sets a Service to Disabled Start Type + +.NOTES + Service must be stopped separately +#> + + [CmdletBinding()] + param( + [Alias("ServiceObject")] + [Parameter(Mandatory=$true)] + [object]$service + ) + + $logLead = (Get-LogLeadName); + $service.Refresh() + + if ($service.StartType -eq "Disabled") + { + Write-Host ("$logLead : Service '{0}' is already disabled, no action necessary" -f $service.Name) + return + } + + Write-Host ("$logLead : Preparing to disable service '{0}'." -f $service.Name) + Write-Verbose ("$logLead : Status before changes:{0}" -f ($service | Select-Object Name, StartType, Status)) + + Set-Service -Name $service.Name -StartupType Disabled + $service = Get-Service $service.Name + + if ($service.StartType -ne "Disabled") { + Write-Warning ("$logLead : Unable to disable the '{0}' Service" -f $service.Name) + } else { + Write-Host ("$logLead : Service disabled. Status after changes: {0}" -f ($service | Select-Object Name, StartType, Status)) + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Disable-Service.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Disable-Service.tests.ps1 new file mode 100644 index 0000000..2f7fa18 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Disable-Service.tests.ps1 @@ -0,0 +1,108 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Disable-Service-Integration +Describe "Disable-Service-Integration" -Tag 'Integration' { + + Context "Validation" { + + # The preceeding space makes it show up at the top of the service list + # Should make it visible in the event of test cleanup failure + $randomFileName = [System.IO.Path]::GetRandomFileName() + $testServiceName = ("_PESTER{0}" -f ($randomFileName.Split(".") | Select-Object -First 1)) + <# + To delete any/all _PESTER services that don't get cleaned up. + get-service -Name _PESTER* |% {(Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $_.Name)) | Invoke-CIMMethod -MethodName Delete} + #> + Write-Warning ("Using Test Service Name '{0}'" -f $testServiceName) + Write-Host "Creating Test Service $testServiceName" + + It "It Disables the Windows Service Provided" { + try + { + Write-Host "Creating Test Service $testServiceName" + New-Service -Name $testServiceName -BinaryPathName "FakePath" + + $svc = Get-Service $testServiceName -ErrorAction SilentlyContinue + + if ($null -eq $svc) + { + throw "The Test Service Could Not Be Created. Check for permissions issues" + } + + Disable-Service $svc + Get-Service $testServiceName | Select-Object StartType | Should Match "Disabled" + } + finally + { + if ($null -ne (Get-Service $testServiceName -ErrorAction SilentlyContinue)) + { + (Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $testServiceName)) | Invoke-CIMMethod -MethodName Delete + } + } + } + } +} + +#endregion Disable-Service-Integration +#region Disable-Service-Unit +Describe "Disable-Service-Unit" -Tag 'Unit' { + Context "Logic" { +#region Arrange + + $randomFileName = [System.IO.Path]::GetRandomFileName() + $testServiceName = ("_PESTER{0}" -f ($randomFileName.Split(".") | Select-Object -First 1)) + <# + To delete any/all _PESTER services that don't get cleaned up. + get-service -Name _PESTER* |% {(Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $_.Name)) | Invoke-CIMMethod -MethodName Delete} + #> + Write-Warning ("Using Test Service Name '{0}'" -f $testServiceName) + Write-Host "Creating Test Service $testServiceName" + + $svc = New-Service -Name $testServiceName -BinaryPathName "FakePath" -StartupType "Manual" + $svc = Get-Service $testServiceName -ErrorAction SilentlyContinue + + Mock -CommandName Stop-AlkamiService -MockWith { return $true } -ModuleName $moduleForMock + Mock -CommandName Set-Service -MockWith { } -ModuleName $moduleForMock + <# + Pester `Mock -MockWith` is ... weird. + It lets you use parameter variable names from *inside* the scope of the namespace you're + mocking the command call *from*. + So $service.Name below is not evaluated until the call to Get-Service from *inside* of + Alkami.PowerShell.Services happens. At which point, $service has a value, as does + $service.Name + https://github.com/pester/Pester/wiki/Mock - where they describe the `Mock-With` parameter. + #> + Mock -CommandName Get-Service -MockWith { + [PSCustomObject]@{ + Name = $service.Name; + StartType = "Disabled"; + Status = "Stopped" + } + } -ModuleName $moduleForMock + + if ($null -eq $svc){ + Set-ItResult -Inconclusive -Because 'Could not create service fake' + } +#endregion Arrange + + It "Calls Set-Service with correct parameters" { +#region Act + Disable-Service $svc +#endregion Act +#region Assert + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -ParameterFilter { $Name -eq $testServiceName -and $StartupType -eq "Disabled" } -Scope It -Exactly 1 +#endregion Assert + } +#region Cleanup + (Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $testServiceName)) | Invoke-CIMMethod -MethodName Delete +#endregion Cleanup + + } +} +#endregion Disable-Service-Unit \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.Tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.Tests.ps1 new file mode 100644 index 0000000..c540a7e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.Tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Enable-Nag-Unit" -Tag 'Unit' { + Context "Logic" { + + Mock -CommandName Enable-Service -MockWith {} -ModuleName $moduleForMock + Mock -CommandName "Get-Service" { + New-MockObject -Type System.ServiceProcess.ServiceController + } -ModuleName $moduleForMock + Mock -CommandName Set-AppSetting -MockWith {} -ModuleName $moduleForMock + + It "Calls Enable-Service with ServiceObject" { + Enable-Nag + Assert-MockCalled -ModuleName $moduleForMock -CommandName Enable-Service -ParameterFilter { $service } -Scope It -Exactly 1 + } + It "Calls Set-AppSetting for TenantFI regex" { + Enable-Nag + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AppSetting -ParameterFilter { $key -eq 'TenantFIWhitelistRegex' -and $value -eq '' } + } + It "Calls Set-AppSetting for JobName regex" { + Enable-Nag + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-AppSetting -ParameterFilter { $key -eq 'JobNameWhitelistRegex' -and $value -eq '' } + } + It "Does nothing if Nag not found" { + Mock -CommandName "Get-Service" {return $null} -ModuleName $moduleForMock + Enable-Nag + Assert-MockCalled -ModuleName $moduleForMock -CommandName Enable-Service -ParameterFilter { $service } -Scope It -Exactly 0 + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.ps1 new file mode 100644 index 0000000..2ce1c57 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Nag.ps1 @@ -0,0 +1,45 @@ +function Enable-Nag { +<# +.SYNOPSIS + Enable Nag by setting the service to Manual and clearing bogus values from the config + +.PARAMETER nagPath + The path where Nag is installed. Defaults to (Get-OrbPath)\Nag + +.NOTES + The Nag service must be started separately +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [string]$nagPath + ) + + $nagServiceName = 'Alkami Nag Service' + $logLead = (Get-LogLeadName) + + if([string]::IsNullOrEmpty($nagPath)) { + $nagPath = (Join-Path(Get-OrbPath) "\Nag") + } + + Write-Verbose "$logLead : Using Nag Path $nagPath" + + $nagService = (Get-Service -Name $nagServiceName -ErrorAction SilentlyContinue) + + if ($null -eq $nagService) { + Write-Warning "$logLead : The |$nagServiceName| is not installed on this machine" + return + } + + # 1. Enable service + Enable-Service -ServiceObject $nagService + + # 2. Un-mangle the config + # + # ToDo: When dev changes the app setting key in ORB we'll remove the term WhiteList from this function + # Until then, we will match the development code base to avoid any confusion + # See: SRE-14133 + $nagConfig = (Join-Path $nagPath "Alkami.App.Nag.Host.Service.exe.config") + Set-AppSetting -key 'TenantFIWhitelistRegex' -value '' -filePath $nagConfig + Set-AppSetting -key 'JobNameWhitelistRegex' -value '' -filePath $nagConfig +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.Tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.Tests.ps1 new file mode 100644 index 0000000..74a211f --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.Tests.ps1 @@ -0,0 +1,25 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Enable-Radium" -Tag 'Unit' { + Context "Logic" { + Mock -CommandName Enable-Service -MockWith {} -ModuleName $moduleForMock + Mock -CommandName "Get-Service" { + New-MockObject -Type System.ServiceProcess.ServiceController + } -ModuleName $moduleForMock + It "Calls Enable-Service with 'Alkami Radium Scheduler Service'" { + Enable-Radium + Assert-MockCalled -ModuleName $moduleForMock -CommandName Enable-Service -ParameterFilter { $service } -Scope It -Exactly 1 + } + It "does nothing if Radium not found" { + Mock -CommandName "Get-Service" {return $null} -ModuleName $moduleForMock + Enable-Radium + Assert-MockCalled -ModuleName $moduleForMock -CommandName Enable-Service -ParameterFilter { $service } -Scope It -Exactly 0 + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.ps1 new file mode 100644 index 0000000..4ae4a4b --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Radium.ps1 @@ -0,0 +1,23 @@ +function Enable-Radium { +<# +.SYNOPSIS + Sets Radium Scheduler service to Manual Start Type + +.NOTES + Radium Scheduler service must be started separately +#> + param( + + ) + + $radiumServiceName = 'Alkami Radium Scheduler Service' + $logLead = (Get-LogleadName); + Write-Verbose "$logLead : Setting |$radiumServiceName| StartupType to |Manual|..." + $radiumService = Get-Service -Name $radiumServiceName + if ($null -eq $radiumService) { + Write-Warning "$logLead : Radium service |$radiumServiceName| is not installed on this host." + return + } + Enable-Service -ServiceObject $radiumService + +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Service.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Service.ps1 new file mode 100644 index 0000000..4b9bb0f --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Service.ps1 @@ -0,0 +1,50 @@ +function Enable-Service { +<# +.SYNOPSIS + Sets a Service to Manual Start Type. Sets Tier-0 Services to Automatic-Delayed Start Type. + +.NOTES + Service must be started separately +#> + + [CmdletBinding()] + param( + [Alias("ServiceObject")] + [Parameter(Mandatory=$true)] + [object]$service + ) + + $logLead = (Get-LogLeadName); + $tier0Services = Get-ServicesByTier -Tier 0 + # Patching jobs don't pass in services, they pass in CimInstances from Get-ChocolateyServices. That breaks. + # So instead, we take any object that gets sent in and get the service that has that name. + # If the service object we get doesn't have a name, then how was any of this supposed to work anyways? + $service = Get-Service $service.Name + + if (($tier0Services -NotContains $service.Name -and $service.StartType -eq "Manual") -or + ($tier0Services -Contains $service.Name -and $service.StartType -eq "Automatic")) { + Write-Host "$logLead : Service is already enabled, no action necessary" + return + } + + Write-Host ("$logLead : Preparing to enable service '{0}'." -f $service.Name) + Write-Verbose ("$logLead : Status before changes:{0}" -f ($service | Select-Object Name, StartType, Status)) + + # Tier 0 is hard coded to Automatic - Delayed -- SRE-17490 + if($tier0Services -Contains $service.Name){ + Write-Host ("$logLead : Service '{0}' is Tier0. Setting to Automatic-Delayed." -f $service.Name) + $params = @("config", $service.Name, "start=delayed-auto") + + Invoke-SCExe $params + } else { + Set-Service -Name $service.Name -StartupType Manual + } + $service = Get-Service $service.Name + + if (($tier0Services -NotContains $service.Name -and $service.StartType -ne "Manual") -or + ($tier0Services -Contains $service.Name -and $service.StartType -ne "Automatic")) { + Write-Warning "$logLead : Unable to enable the $service.Name Service" + } else { + Write-Host ("$logLead : Service enabled. Status after changes: {0}" -f ($service | Select-Object Name, StartType, Status)) + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Enable-Service.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Enable-Service.tests.ps1 new file mode 100644 index 0000000..d7a3fa8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Enable-Service.tests.ps1 @@ -0,0 +1,121 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Enable-Service-Unit +Describe "Enable-Service-Unit" -Tag 'Unit' { + Context "Logic" { +#region Arrange + + $randomFileName = [System.IO.Path]::GetRandomFileName() + $testServiceName = ("_FAKE{0}" -f ($randomFileName.Split(".") | Select-Object -First 1)) + Write-Warning ("Using Test Service Name '{0}'" -f $testServiceName) + Write-Host "Creating Test Service $testServiceName" + + $svc = New-Service -Name $testServiceName -BinaryPathName "FakePath" -StartupType "Disabled" + $svc = Get-Service $testServiceName -ErrorAction SilentlyContinue + + # Write warnings as part of this function if setting the service fails, but since we don't actually change the service + # type in the Set-Service mock the warnings always fire in the tests. So just suprress them here. + Mock -CommandName Write-Host -MockWith { } -ModuleName $moduleForMock + Mock -CommandName Write-Warning -MockWith { } -ModuleName $moduleForMock + Mock -CommandName Set-Service -MockWith { } -ModuleName $moduleForMock + <# + Pester `Mock -MockWith` is ... weird. + It lets you use parameter variable names from *inside* the scope of the namespace you're + mocking the command call *from*. + So $service.Name below is not evaluated until the call to Get-Service from *inside* of + Alkami.PowerShell.Services happens. At which point, $service has a value, as does + $service.Name + https://github.com/pester/Pester/wiki/Mock - where they describe the `Mock-With` parameter. + #> + Mock -CommandName Get-Service -MockWith { + [PSCustomObject]@{ + Name = $service.Name; + StartType = "Disabled"; + Status = "Stopped" + } + } -ModuleName $moduleForMock + + if ($null -eq $svc){ + Set-ItResult -Inconclusive -Because 'Could not create service fake' + } +#endregion Arrange + + It "Calls Set-Service with Disabled Service" { +#region Act + Enable-Service $svc +#endregion Act +#region Assert + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -ParameterFilter { $Name -eq $testServiceName -and $StartupType -eq "Manual" } -Scope It -Exactly 1 +#endregion Assert + } + + It "Calls Set-Service with an Auto Service" { + Mock -CommandName Get-Service -MockWith { + [PSCustomObject]@{ + Name = $service.Name; + StartType = "Automatic"; + Status = "Stopped" + } + } -ModuleName $moduleForMock + #region Act + Enable-Service $svc + #endregion Act + #region Assert + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -ParameterFilter { $Name -eq $testServiceName -and $StartupType -eq "Manual" } -Scope It -Exactly 1 + #endregion Assert + } + + It "Calls Invoke-SCExe with a Tier-0 service"{ + Mock -CommandName Get-Service -MockWith { + [PSCustomObject]@{ + Name = $service.Name; + StartType = "Disabled"; + Status = "Stopped" + } + } -ModuleName $moduleForMock + + Mock -CommandName Invoke-SCExe -MockWith { } -ModuleName $moduleForMock + + Mock -CommandName Get-ServicesByTier -MockWith { return $service.Name } -ModuleName $moduleForMock + + #region Act + Enable-Service $svc + #endregion Act + #region Assert + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-SCExe -Scope It -Exactly 1 + #endregion Assert + } + + It "Calls Set-Service with a Manual Service" { + Mock -CommandName Get-Service -MockWith { + [PSCustomObject]@{ + Name = $service.Name; + StartType = "Manual"; + Status = "Stopped" + } + } -ModuleName $moduleForMock + Mock -CommandName Get-ServicesByTier -MockWith { } -ModuleName $moduleForMock + + +#region Act + Enable-Service $svc +#endregion Act +#region Assert + Assert-MockCalled -ModuleName $moduleForMock -CommandName Set-Service -ParameterFilter { $Name -eq $testServiceName -and $StartupType -eq "Manual" } -Scope It -Exactly 0 +#endregion Assert + +# Multiple tests use the service, only clean up in the last one. + +#region Cleanup + (Get-CIMInstance Win32_Service -filter ("name='{0}'" -f $testServiceName)) | Invoke-CIMMethod -MethodName Delete +#endregion Cleanup + } + } +} +#endregion Enable-Service-Unit \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-AlkamiServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-AlkamiServices.ps1 new file mode 100644 index 0000000..5b3403c --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-AlkamiServices.ps1 @@ -0,0 +1,19 @@ +function Get-AlkamiServices { +<# +.SYNOPSIS + Returns installed Alkami Services +#> + [CmdletBinding()] + Param( + [switch]$includeDisabled + ) + + $alkamiServices = Get-Service | Where-Object {$_.Name -match "Alkami.+"} + + if ($includeDisabled.IsPresent) { + return $alkamiServices + } + else { + return $alkamiServices | Where-Object { $_.StartType -ne "Disabled" } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.ps1 new file mode 100644 index 0000000..21e7d85 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.ps1 @@ -0,0 +1,14 @@ +function Get-AppTierServices { +<# +.SYNOPSIS + Used to get the list of services used during things like Install\-ORBAppServer + The benefit of this function is to inject more easily for unit testing, and encapsulation + This lets us retrieve the values for console testing as well, so we can see what is configured +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + ) + + return $global:appTierServices +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.tests.ps1 new file mode 100644 index 0000000..666fdcd --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-AppTierServices.tests.ps1 @@ -0,0 +1,21 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-AppTierServices" { + Context "Ensure that it returns some values" { + $values = Get-AppTierServices + + It "should have some values" { + $values | Should -Not -BeNullOrEmpty + } + } + + Context "Ensure that it does not throw" { + { Get-AppTierServices } | Should -Not -Throw + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.ps1 new file mode 100644 index 0000000..0efbcb1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.ps1 @@ -0,0 +1,115 @@ +function Get-ChocolateyServices { +<# +.SYNOPSIS + Returns Service Objects for Services Running out of the Chocolatey Path. By default it does not return disabled services. Defaults to running against the current machine. + +.DESCRIPTION + Returns Service Objects for Services Running out of the Chocolatey Path. By default it does not return disabled services but can be overridden by using the -includeDisabled switch. Defaults to running against the local machine but can be overridden by passing the RemoteServer parameter. + +.PARAMETER IncludeDisabled + [switch] When supplied, the function returns disabled services in the output + +.PARAMETER RemoteServer + [string When supplied executes against a single remote machine. Defaults to localhost + +.EXAMPLE + Get-ChocolateyServices + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. + +ProcessId Name StartMode State Status ExitCode +--------- ---- --------- ----- ------ -------- +0 Alkami.Services.Subscriptions.Host Auto Stopped OK 0 +0 Alkami.MicroServices.AccountBalanceSync1.Processor.Host Auto Stopped OK 0 +0 Alkami.MicroServices.AccountBalanceSync2.Processor.Host Auto Stopped OK 0 +0 Alkami.MicroServices.Accounts.Service.Host Auto Stopped OK 0 + +.EXAMPLE + Get-ChocolateyServices -IncludeDisabled + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 5 chocolatey services. + +ProcessId Name StartMode State Status ExitCode +--------- ---- --------- ----- ------ -------- +0 Alkami.Services.Subscriptions.Host Auto Stopped OK 0 +0 Alkami.MicroServices.AccountBalanceSync1.Processor.Host Auto Stopped OK 0 +0 Alkami.MicroServices.AccountBalanceSync2.Processor.Host Auto Stopped OK 0 +0 Alkami.MicroServices.Accounts.Service.Host Auto Stopped OK 0 +0 Alkami.MicroServices.AchTemplates.Service.Host Disabled Stopped OK 0 + +#> + [CmdletBinding()] + Param( + [switch]$IncludeDisabled, + #defaults to local computer if not specified + [string]$RemoteServer = "localhost" + ) + + $logLead = (Get-LogLeadName) + $chocoInstallPath = (Get-ChocolateyInstallPath) + Write-Host "$loglead : Finding services installed out of the chocolatey path: $chocoInstallPath" + + $chocoPathFilter = $chocoInstallPath.Replace("\", "\\") + $disabledFilter = if ($IncludeDisabled.IsPresent) { + "" + } else { + + "AND StartMode <> 'Disabled'" + } + + $isLocal = Compare-StringToLocalMachineIdentifiers $RemoteServer + $cimFilter = "PathName LIKE '%$chocoPathFilter%' $disabledFilter" + Write-Verbose "$logLead : Using CIMInstance Filter: $cimFilter" + + # Including ComputerName in this call adds ~2.5s -- even to localhost. Don't change this + if ($isLocal) { + + [array]$services = Get-CIMInstance win32_service -Filter $cimFilter + } else { + + [array]$services = Get-CIMInstance win32_service -ComputerName $RemoteServer -Filter $cimFilter + } + + # Sort subscription service to the front just in case this list is being used to start services. + $subscriptionServiceName = "Alkami.Services.Subscriptions.Host" + $subscriptionService = $services | Where-Object { $_.Name -like $subscriptionServiceName } + if($subscriptionService) + { + $updatedList = @($subscriptionService) + $updatedList += ($services | Where-Object { $_.Name -notlike $subscriptionServiceName }) + [array]$services = $updatedList + } + + if (Test-IsCollectionNullOrEmpty $services) { + + Write-Warning "$logLead : No Chocolatey Services Found in: $chocoInstallPath" + return $null + } + + # Write-Warning for each Service With No Files on Disk + foreach ($service in $services) { + + if ($service.PathName -match '"') { + + $path = $service.PathName.split('"')[1] + } else { + + $path = $service.PathName + } + + if(!(Test-Path $path)) { + + Write-Warning "Service $($service.Name) has nonexistent file path `"$path`". Was it improperly uninstalled? Delete this service!" + continue + } + } + + if (!(Test-IsCollectionNullOrEmpty $services)) { + + Write-Host "$loglead : Found $($services.count) chocolatey services." + } + + return $services +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.tests.ps1 new file mode 100644 index 0000000..c35f473 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServices.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ChocolateyServices" { + + Context "Logic" { + + It "Should warn if Chocolatey services point to non-existant file locations" { + + Mock -ModuleName $moduleForMock Get-ChocolateyInstallPath -MockWith { return "C:\ProgramData\Chocolatey\lib"} + Mock -ModuleName $moduleForMock -CommandName Get-CIMInstance { return @( + @{ State = "Stopped"; Name = "Alkami.Bogus.Service"; PathName = "`"C:\ProgramData\Chocolatey\lib\BogusExe.exe`" -displayname `"Alkami.Bogus.Service`" -servicename `"Alkami.Bogus.Service"; StartMode = "Manual"; } + ) } + + Mock -ModuleName $moduleForMock Write-Warning -MockWith { } + + Get-ChocolateyServices + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "has nonexistent file path" } + } + + It "Should warn and return null if no matching services found" { + + Mock -ModuleName $moduleForMock Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-CIMInstance { return @() } + + $null -eq (Get-ChocolateyServices) | Should -BeTrue + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "No Chocolatey Services Found in" } + } + + It "Should Use the System Choco Path" { + + Mock -ModuleName $moduleForMock Test-Path -MockWith { return $true } + Mock -ModuleName $moduleForMock Get-ChocolateyInstallPath -MockWith { return "C:\BogusChoco"} + Mock -ModuleName $moduleForMock Get-CIMInstance -MockWith {} + + Get-ChocolateyServices + Assert-MockCalled -ModuleName $moduleForMock Get-ChocolateyInstallPath -Times 1 -Exactly -Scope It + Assert-MockCalled -ModuleName $moduleForMock Get-CIMInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Filter -match "BogusChoco" } + } + + It "Uses CIM Filters to Exclude Disabled Services By Default" { + + Mock -ModuleName $moduleForMock Get-ChocolateyInstallPath -MockWith { return "C:\ProgramData\Chocolatey\lib"} + Mock -ModuleName $moduleForMock -CommandName Get-CIMInstance { } + + Get-ChocolateyServices + Assert-MockCalled -ModuleName $moduleForMock Get-CIMInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Filter -match "StartMode <> 'Disabled'" } + } + + It "Does Not Use CIM Filters to Exclude Disabled Services When -IncludeDisabled Is Specified" { + + Mock -ModuleName $moduleForMock Get-ChocolateyInstallPath -MockWith { return "C:\ProgramData\Chocolatey\lib"} + Mock -ModuleName $moduleForMock -CommandName Get-CIMInstance { } + + Get-ChocolateyServices -IncludeDisabled + Assert-MockCalled -ModuleName $moduleForMock Get-CIMInstance -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Filter -notmatch "StartMode <> 'Disabled'" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.ps1 new file mode 100644 index 0000000..70cab55 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.ps1 @@ -0,0 +1,95 @@ +function Get-ChocolateyServicesToStart { +<# +.SYNOPSIS + Returns service objects for services running out of the Chocolatey path, decorated by tier, which are in the stopped state. + +.DESCRIPTION + Returns service objects for services running out of the Chocolatey path, decorated by tier, which are in the stopped state. Includes the path for the service executable. + +.PARAMETER RemoteServer + [string When supplied executes against a single remote machine. Defaults to localhost + +.EXAMPLE + Get-ChocolateyServicesToStart -Verbose + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. +[Get-ChocolateyServicesToStart] : Found 4 Chocolatey Services +VERBOSE: [Get-ChocolateyServicesToStart] : Pulling Services for Tier 0 +VERBOSE: [Get-ChocolateyServicesToStart] : Found 2 Services for Tier 0 +VERBOSE: [Get-ChocolateyServicesToStart] : Adding Service Alkami.Services.Subscriptions.Host to Tier 0 +VERBOSE: [Get-ChocolateyServicesToStart] : Alkami.MicroServices.Broker.Host is a tier 0 service but cannot be started due to current state Running +VERBOSE: [Get-ChocolateyServicesToStart] : Pulling Services for Tier 1 +VERBOSE: [Get-ChocolateyServicesToStart] : Found 14 Services for Tier 1 +VERBOSE: [Get-ChocolateyServicesToStart] : Adding Service Alkami.MicroServices.Authorization.Service.Host to Tier 1 +VERBOSE: [Get-ChocolateyServicesToStart] : Pulling Services for Tier 2 +VERBOSE: [Get-ChocolateyServicesToStart] : No Services Defined for Tier 2 -- adding all other services not matched +VERBOSE: [Get-ChocolateyServicesToStart] : Adding Service Alkami.MicroServices.Features.Beacon.Host to Tier 2 + +ServiceName ServicePath +----------- ----------- +Alkami.Services.Subscriptions.Host "C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host\to... +Alkami.MicroServices.Authorization.Service.Host "C:\ProgramData\chocolatey\lib\Alkami.MicroServices.Authorization.Se... +Alkami.MicroServices.Features.Beacon.Host "C:\ProgramData\chocolatey\lib\Alkami.MicroServices.Features.Beacon.... +#> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + [string]$RemoteServer = "localhost" + ) + + $logLead = (Get-LogLeadName) + [array]$chocolateyServices = Get-ChocolateyServices $RemoteServer + Write-Verbose "$logLead : Found $($chocolateyServices.Count) Chocolatey Services" + + $microserviceTiers = (Get-MicroServiceTiers) + $tieredServices = @() + + for ($i = 0; $i -le ($microserviceTiers).Count; $i++) { + + Write-Verbose "$logLead : Pulling Services for Tier $i" + $matchedServiceNames = $tieredServices | Select-Object -ExpandProperty ServiceName + + if (Test-IsCollectionNullOrEmpty $microServiceTiers[$i]) { + + # We hit the end, return everything else not defined + Write-Verbose "$logLead : No Services Defined for Tier $i -- adding all other services not matched" + $allOtherServices = $chocolateyServices | Where-Object { ($_.State -eq "Stopped") -and ($matchedServiceNames -notcontains $_.Name) } + + foreach ($service in $allOtherServices) { + + Write-Verbose "$logLead : Adding Service $($service.Name) to Tier $i" + $tieredServices += New-Object PSObject -Property @{ ServiceName = $service.Name; Tier=$i; ServicePath = $service.PathName; } + } + } else { + + Write-Verbose "$logLead : Found $(($microserviceTiers[$i]).Count) Services for Tier $i" + foreach ($tierService in $microserviceTiers[$i]) { + + $serviceDefinition = $chocolateyServices | Where-Object {$_.Name -eq $tierService} + + if ($null -eq $serviceDefinition) { + + if (($i -eq 0 -and !(Test-IsServiceFabricServer)) -or + (Test-IsMicroServer)) + { + # Tier 0 Services Should Exist Everywhere Except FAB Nodes + # Any other Tiered service should exist on Microservice boxes + Write-Warning "$logLead : $tierService is defined in tier $i but not located on this machine" + } + + } elseif ($serviceDefinition.State -ne "Stopped") { + + Write-Verbose "$logLead : $tierService is a tier $i service but cannot be started due to current state $($serviceDefinition.State)" + } else { + + Write-Verbose "$logLead : Adding Service $($serviceDefinition.Name) to Tier $i" + $tieredServices += New-Object PSObject -Property @{ ServiceName = $serviceDefinition.Name; Tier=$i; ServicePath = $serviceDefinition.PathName; } + } + } + } + } + + Write-Host "$logLead : Found $($tieredServices.Count) Chocolatey Services to Start" + return $tieredServices +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.tests.ps1 new file mode 100644 index 0000000..f4d6b75 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ChocolateyServicesToStart.tests.ps1 @@ -0,0 +1,143 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ChocolateyServicesToStart" { + + Context "Logic" { + + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return @( + @{Name = "Alkami.Tier0.Service"; State = "Stopped"; PathName = "C:\Tier0.exe"; }, + @{Name = "Alkami.Tier1.Service"; State = "Stopped"; PathName = "C:\Tier1.exe"; }, + @{Name = "Alkami.Tier2.Service"; State = "Stopped"; PathName = "C:\Tier2.exe"; }, + @{Name = "Alkami.Tier3.Service"; State = "Stopped"; PathName = "C:\Tier2.exe"; }, + @{Name = "Alkami.Running.Service"; State = "Running"; PathName = "C:\Running.exe"; } + ) + } + + Mock -CommandName Get-MicroServiceTiers -ModuleName $moduleForMock -MockWith { + + return @( + @( + "Alkami.Tier0.Service" + ), + @( + "Alkami.Tier1.Service", + "Alkami.Running.Service" + ) + ) + } + + $s = Get-ChocolateyServicesToStart + + It "Assigns Tier Based on Order in the Array" { + + $s | Where-Object {$_.ServiceName -eq "Alkami.Tier0.Service"} | Select-Object -ExpandProperty Tier | Should -Be 0 + $s | Where-Object {$_.ServiceName -eq "Alkami.Tier1.Service"} | Select-Object -ExpandProperty Tier | Should -Be 1 + $s | Where-Object {$_.ServiceName -eq "Alkami.Tier2.Service"} | Select-Object -ExpandProperty Tier | Should -Be 2 + $s | Where-Object {$_.ServiceName -eq "Alkami.Tier3.Service"} | Select-Object -ExpandProperty Tier | Should -Be 2 + } + + It "Excludes Services Already Running" { + + $s | Where-Object {$_.ServiceName -eq "Alkami.Running.Service"} | Should -BeNull + } + + It "Dynamically Lumps All Undefined Services in the Last Tier" { + + Mock -CommandName Get-MicroServiceTiers -ModuleName $moduleForMock -MockWith { + + # This data pretends we added a new "Tier 2" definition, which should put Tier3 in to Tier 3 instead of Tier 2 as in the tests above / current state + return @( + @( + "Alkami.Tier0.Service" + ), + @( + "Alkami.Tier1.Service", + "Alkami.Running.Service" + ), + @( + "Alkami.Tier2.Service" + ) + ) + } + + $s = Get-ChocolateyServicesToStart + $s | Where-Object {$_.ServiceName -eq "Alkami.Tier3.Service"} | Select-Object -ExpandProperty Tier | Should -Be "3" + } + + It "Calls Get Chocolatey Services with the Supplied Machine Name" { + + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith {} + + $fakeMachine = "FakeMachine.FQDN.Com" + Get-ChocolateyServicesToStart $fakeMachine + + Assert-MockCalled -ModuleName $moduleForMock Get-ChocolateyServices -Times 1 -Exactly -Scope It ` + -ParameterFilter { $RemoteServer -eq $fakeMachine } + } + } + + Context "Warnings" { + + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ChocolateyServices -ModuleName $moduleForMock -MockWith { + + return @( + @{Name = "Alkami.Legitimate.Tier0.Service"; State = "Stopped"; PathName = "C:\LegitTier0Service.exe"; }, + @{Name = "Alkami.Legitimate.Tier1.Service"; State = "Stopped"; PathName = "C:\LegitTier1Service.exe"; } + ) + } + + Mock -CommandName Get-MicroServiceTiers -ModuleName $moduleForMock -MockWith { + + return @( + @( + "Alkami.NonExistant.Tier0.Service" + ), + @( + "Alkami.Legitimate.Tier1.Service", + "Alkami.NonExistant.Tier1.Service" + ) + ) + } + + It "Writes a Warning if a Service is in Tier 0 But Is Not Found on Non FAB Servers" { + + Mock -CommandName Test-IsServiceFabricServer -ModuleName $moduleForMock -MockWith { return $false } + $s = Get-ChocolateyServicesToStart + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Alkami.NonExistant.Tier0.Service is defined in tier 0 but not located on this machine" } + } + + It "Does Not Write a Warning if a Service is in Tier 0 But Is Not Found on Non FAB Servers" { + + Mock -CommandName Test-IsServiceFabricServer -ModuleName $moduleForMock -MockWith { return $true } + $s = Get-ChocolateyServicesToStart + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Alkami.NonExistant.Tier0.Service is defined in tier 0 but not located on this machine" } + } + + It "Writes a Warning if a Service is in Tier 1 But Is Not Found on MicroService Servers" { + + Mock -CommandName Test-IsMicroServer -ModuleName $moduleForMock -MockWith { return $true } + $s = Get-ChocolateyServicesToStart + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Alkami.NonExistant.Tier1.Service is defined in tier 1 but not located on this machine" } + } + + It "Does Not Write a Warning if a Service is in Tier 1 But Is Not Found on Non MicroService Servers" { + + Mock -CommandName Test-IsMicroServer -ModuleName $moduleForMock -MockWith { return $false } + $s = Get-ChocolateyServicesToStart + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 0 -Exactly -Scope It ` + -ParameterFilter { $Message -match "Alkami.NonExistant.Tier1.Service is defined in tier 1 but not located on this machine" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsService.ps1 new file mode 100644 index 0000000..817be0e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsService.ps1 @@ -0,0 +1,58 @@ +function Get-FileBeatsService { +<# +.SYNOPSIS + Returns list of installed FileBeat Services + +.PARAMETER SearchPrefix + Additional refining prefix for searching. When not present, defaults to checking the paths returned by Get-FileBeatsPath + +.PARAMETER IncludeDisabled + Should include disabled services in the output + +.OUTPUTS + Returns the same data format structure as Get-ServiceInfoByCIMFragment +#> + [CmdletBinding()] + [OutputType([string[]])] + Param( + [Parameter(Mandatory = $false)] + [string[]]$SearchPrefix, + + [switch]$IncludeDisabled + ) + + $logLead = (Get-LogLeadName) + + $disabledStartMode = "Disabled" + + if ([string]::IsNullOrWhiteSpace($SearchPrefix)) { + # We know the service is likely registered under at least one of these paths + # It is entirely possible to have two services installed on one machine under one or more paths + + # This is either returned as an @array OR $null - do NOT force it to @(array) or you break the world + # because your @(array) can come back as an error technically "empty" and "null" even though it has + # a single element, which just happens to be null + $SearchPrefix = Get-FileBeatsPath + } + + if (Test-IsCollectionNullOrEmpty $SearchPrefix) { + Write-Warning "$logLead : Could not find any properties/paths to check for FileBeats Services. Is the service installed?" + return @() + } + + $services = @() + foreach($pathOrPrefix in $SearchPrefix) { + $service = (Get-ServiceInfoByCIMFragment -QueryFragment $pathOrPrefix) + if ($null -ne $service) { + # if the flag to include disabled services is passed _and_ the service is disabled, return it + # if the flag to include disabled services is NOT passed _and_ the service is disabled, DO NOT return it + if ((!$includeDisabled -and $service.StartMode -ne $disabledStartMode) -or ($includeDisabled -and $service.StartMode -eq $disabledStartMode)) { + $services += $service + } else { + Write-Host "$logLead : Found service [$($service.DisplayName)] but it was disabled and disabled services are not included in the return results. To include in the future, use the flag -IncludeDisabled" + } + } + } + + return $services +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsServicePaths.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsServicePaths.ps1 new file mode 100644 index 0000000..ce0ab1d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-FileBeatsServicePaths.ps1 @@ -0,0 +1,23 @@ +function Get-FileBeatsServicePaths { +<# +.SYNOPSIS + Returns list of installed Filebeat Service exe paths + +.PARAMETER FileBeatService + The services for FileBeats. If not provided, will be looked up in the default location(s). +#> + [CmdletBinding()] + [OutputType([string[]])] + Param( + [Parameter(Mandatory = $false)] + [object[]]$FileBeatService + ) + + if (Test-IsCollectionNullOrEmpty $FileBeatService) { + [array]$FileBeatService = @(Get-FileBeatsService) + } + + [array]$filebeatServicePaths = @($FileBeatService.ExePath) + + return $filebeatServicePaths +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-NagTriggersCount.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-NagTriggersCount.ps1 new file mode 100644 index 0000000..4ff611b --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-NagTriggersCount.ps1 @@ -0,0 +1,42 @@ +function Get-NagTriggersCount { +<# +.SYNOPSIS + Returns a count of rows from NAG_TRIGGERS from the master database. +#> + param( + [Parameter(Position = 0, Mandatory = $false)] + [string]$ConnectionString + ) + if ([String]::IsNullOrEmpty($ConnectionString)) { + $masterConnectionString = Get-MasterConnectionString + } + else { + $masterConnectionString = $ConnectionString + } + + $conn = New-Object System.Data.SqlClient.SqlConnection + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + + $conn.ConnectionString = $conStrBuilder.ToString() + + $triggerQuery = "SELECT COUNT(*) FROM dbo.NAG_TRIGGERS" + $cmd = New-Object System.Data.SqlClient.SqlCommand($triggerQuery, $conn) + + try + { + $conn.Open() + $sqlReader = $cmd.ExecuteScalar() + return $sqlReader.ToString() + } + finally + { + # Cleanup the System.Data.SqlClient objects + if ($conn.State -ne [System.Data.ConnectionState]::Closed) + { + $conn.Close() + } + $conn = $null + } +} + + diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.ps1 new file mode 100644 index 0000000..54e1960 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.ps1 @@ -0,0 +1,77 @@ +function Get-ProcessFromService { +<# +.SYNOPSIS + Retrieves the process for a Windows service by first interrogating CIM for the service properties + +.PARAMETER Service + This should be an object with at minimum the [name] of a registered service object +#> + [CmdletBinding()] + [OutputType([System.Diagnostics.Process[]])] + Param( + [Parameter(Mandatory=$true)] + [object]$Service + ) + + $logLead = (Get-LogLeadName) + + $imagePaths = @() + $exeNames = @() + $processes = @() + $pids = @() + + $services = @(Get-ServiceInfoByCIMFragment $Service.Name) + + if (Test-IsCollectionNullOrEmpty $services) { + Write-Warning "$logLead : No services returned from [Get-ServiceInfoByCIMFragment $($Service.Name)]" + } + + foreach ($cimService in $services) { + if ($null -eq $cimService) { + Write-Warning "$logLead : Empty object returned from [Get-ServiceInfoByCIMFragment $($Service.Name)]" + } else { + # Ensure the path is "clean" for OS needs + $exePath = $cimService.ExePath + if ([string]::IsNullOrWhiteSpace($exePath)) { + Write-Warning "$logLead : No exePath found for the CIM response on [Get-ServiceInfoByCIMFragment $($Service.Name)]" + } else { + $imagePath = [System.IO.Path]::GetFullPath($exePath) + $exeName = [System.IO.Path]::GetFileNameWithoutExtension($imagePath) + + # should be a nonzero value if running and zero if stopped. Could be null maybe. + if (($null -ne $cimService.ProcessID) -and ($cimService.ProcessID -gt 0)) { + $pids += $cimService.ProcessID + } + + if ($exeNames -notcontains $exeName) { + $exeNames += $exeName + } + if ($imagePaths -notcontains $imagePath) { + $imagePaths += $imagePath + } + } + } + } + + if (!(Test-IsCollectionNullOrEmpty $pids)) { + return (Get-Process -Id $pids) + } + + if (Test-IsCollectionNullOrEmpty $imagePaths) { + Write-Warning "$logLead : No service paths found to process. Returning without results." + return + } + + # because we have an imagePath(s) we therefore must have an exeName(s) + + $processes = (Get-Process | Where-Object {$imagePaths -contains $PSItem.Path }) + + if ($processes.Count -gt 0) { + return $processes + } + + Write-Host "$logLead : Falling back to the legacy lookup pattern" + + # fallback to legacy lookup pattern on filename alone + return (Get-Process -Name $exeNames) +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.tests.ps1 new file mode 100644 index 0000000..86e668d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ProcessFromService.tests.ps1 @@ -0,0 +1,611 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-ProcessFromService" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith {} + Mock -ModuleName $moduleForMock Get-Process -MockWith {} + Mock -ModuleName $moduleForMock Get-Process -ParameterFilter { $Name } -MockWith {} + Mock -ModuleName $moduleForMock Write-Host -MockWith {} + Mock -ModuleName $moduleForMock Write-Warning -MockWith {} + + Context "no CIM instance" { + $fakeService = @{ Name = "fakeService" } + $emptyArray = Get-ProcessFromService $fakeService + + It "Should return an empty value" { + (Test-IsCollectionNullOrEmpty $emptyArray) | Should -BeTrue + } + + It "Did actually warn us" { + Assert-MockCalled -ModuleName $moduleForMock Write-Warning + } + } + + Context "no processes but CIM instance" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { return @{ + Name = "fakeService" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } } + + $fakeService = @{ Name = "fakeService" } + $emptyArray = Get-ProcessFromService $fakeService + + It "Should return an empty value" { + (Test-IsCollectionNullOrEmpty $emptyArray) | Should -BeTrue + } + } + + Context "happy path 1 process and 1 CIM instance" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { return @{ + Name = "fakeService" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } } + + Mock -ModuleName $moduleForMock Get-Process -ParameterFilter { $Name -eq "fakeService" } -MockWith { + param ($Name) + return @{ + Handlecount = 1 + ProcessName = $Name + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\Fake\Path\fakeService.exe" + } + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234" { + $processExists.Id | Should -Be 1234 + } + } + + Context "2 processes but only 1 CIM instance" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { return @{ + Name = "fakeService" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } } + + Mock -ModuleName $moduleForMock Get-Process -MockWith { + param ($Name) + return @( + @{ + Handlecount = 1 + ProcessName = $Name + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\Fake\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "NOT IT" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9999 + Path = "C:\Whoa\Partner\Wrong\NotYourService.exe" + } + ) + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234" { + $processExists.Id | Should -Be 1234 + } + } + + Context "many processes and 2 CIM instances but only one running" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { + return @( + @{ + Name = "fakeService (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + },@{ + Name = "fakeService_os (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path_os\fakeService.exe" + ExePath = "C:\Fake\Path_os\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } + ) + } + + Mock -ModuleName $moduleForMock Get-Process -MockWith { + return @( + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\Fake\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "NotYourService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9999 + Path = "C:\Whoa\Partner\Wrong\NotYourService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "anotherNotService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9998 + Path = "C:\Whoa\Partner\Yikes\anotherNotService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "moreWrong" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9997 + Path = "C:\Whoa\Partner\Again\moreWrong.exe" + }, + @{ + Handlecount = 1 + ProcessName = "blue" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9996 + Path = "C:\Whoa\Partner\ISay\blue.exe" + } + ) + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234" { + $processExists.Id | Should -Be 1234 + } + } + + Context "many processes and 2 CIM instances and both running" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { + return @( + @{ + Name = "fakeService (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + },@{ + Name = "fakeService_os (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path_os\fakeService.exe" + ExePath = "C:\Fake\Path_os\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } + ) + } + + Mock -ModuleName $moduleForMock Get-Process -MockWith { + return @( + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\Fake\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 5555 + Path = "C:\Fake\Path_os\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "NotYourService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9999 + Path = "C:\Whoa\Partner\Wrong\NotYourService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "anotherNotService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9998 + Path = "C:\Whoa\Partner\Yikes\anotherNotService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "moreWrong" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9997 + Path = "C:\Whoa\Partner\Again\moreWrong.exe" + }, + @{ + Handlecount = 1 + ProcessName = "blue" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9996 + Path = "C:\Whoa\Partner\ISay\blue.exe" + } + ) + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234 and 5555" { + $processExists.Id | Should -Be (1234,5555) + } + } + + Context "many processes and 2 CIM instances and both running with returned pids" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { + return @( + @{ + Name = "fakeService (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = 1234 + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + },@{ + Name = "fakeService_os (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path_os\fakeService.exe" + ExePath = "C:\Fake\Path_os\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = 5555 + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } + ) + } + + Mock -ModuleName $moduleForMock Get-Process -ParameterFilter { $Id } -MockWith { + return @( + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\Fake\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 5555 + Path = "C:\Fake\Path_os\fakeService.exe" + } + ) + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234 and 5555" { + $processExists.Id | Should -Be (1234,5555) + } + } + + Context "many processes and 2 CIM instances and both running but legacy lookup by name" { + Mock -ModuleName $moduleForMock Get-ServiceInfoByCIMFragment -MockWith { + return @( + @{ + Name = "fakeService (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path\fakeService.exe" + ExePath = "C:\Fake\Path\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + },@{ + Name = "fakeService_os (Haystack)" + DisplayName = "Definitely a fake service" + Path = "C:\Fake\Path_os\fakeService.exe" + ExePath = "C:\Fake\Path_os\fakeService.exe" + Started = $true + State = "Running" + Status = "OK" + StartMode = "Manual" + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = "Manual" + ProcessId = $null + InstallDate = $null + UserName = "NT AUTHORITY\FakeService" + } + ) + } + + Mock -ModuleName $moduleForMock Get-Process -MockWith { + return @( + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\other\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 5555 + Path = "C:\other\Path_os\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "NotYourService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9999 + Path = "C:\Whoa\Partner\Wrong\NotYourService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "anotherNotService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9998 + Path = "C:\Whoa\Partner\Yikes\anotherNotService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "moreWrong" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9997 + Path = "C:\Whoa\Partner\Again\moreWrong.exe" + }, + @{ + Handlecount = 1 + ProcessName = "blue" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 9996 + Path = "C:\Whoa\Partner\ISay\blue.exe" + } + ) + } + + + + Mock -ModuleName $moduleForMock Get-Process -ParameterFilter { $Name } -MockWith { + return @( + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 1234 + Path = "C:\other\Path\fakeService.exe" + }, + @{ + Handlecount = 1 + ProcessName = "fakeService" + NonpagedSystemMemorySize64 = 1 + PagedMemorySize64 = 1 + SessionId = 1 + VirtualMemorySize64 = 1 + WorkingSet64 = 1 + Id = 5555 + Path = "C:\other\Path_os\fakeService.exe" + } + ) + } + + $fakeService = @{ Name = "fakeService" } + $processExists = Get-ProcessFromService $fakeService + + It "Should return a process item" { + $processExists | Should -Not -BeNull + } + + It "Should have process id 1234 and 5555" { + $processExists.Id | Should -Be (1234,5555) + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServiceByChocoName.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceByChocoName.ps1 new file mode 100644 index 0000000..b368367 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceByChocoName.ps1 @@ -0,0 +1,52 @@ +function Get-ServiceByChocoName { +<# +.SYNOPSIS + Returns a service instance by a chocolatey package name. +.PARAMETER ChocolateyName + Chocolatey Package name to search for +.PARAMETER IncludeDisabled + Include services with StartMode set to Disabled +#> + Param( + [Parameter(Mandatory=$true)] + [string]$ChocolateyName, + [Parameter(Mandatory=$false)] + [switch]$IncludeDisabled + ) + + $logLead = Get-LogLeadName + Write-Verbose "$logLead : Searching for chocolatey service $ChocolateyName" + + # This lookup takes 1ms to perform, the below lookup takes ~400ms to perform. + $existingService = Get-Service -Name $ChocolateyName -ErrorAction SilentlyContinue + if ($null -ne $existingService) { + if ($existingService.StartType -eq 'Disabled' -and -not $IncludeDisabled) { + Write-Warning "$logLead : Chocolatey service $ChocolateyName was found, but is disabled. Returning $null" + return $null + } + # match the prior output with what we have here + # Most callers only want the name. Set- MicroserviceConfigurationBasedState wants the other fields. + return @{ + Name = $existingService.Name + StartMode = $existingService.StartType + State = $existingService.Status + } + } + + $chocolateyInstallPath = Get-ChocolateyInstallPath + $chocolateyLibPath = Join-Path -Path $chocolateyInstallPath -ChildPath "lib" + $chocolateyServicePath = Join-Path -Path $chocolateyLibPath -ChildPath $ChocolateyName + # Trailing backslash to prevent matching a substring of a packagename like alkami.ms.parent matching alkami.ms.parent.child + $chocolateyServicePathString = "$chocolateyServicePath\" + + $servicePath = $chocolateyServicePathString.Replace("\", "\\") + + # TODO: Should this use Get-ServiceInfoByCIMFragment to consolidate usages of Get-CIMInstance Win32_Service since it already matches on path for this scenario? -- cbrand ~ 2022/06/22 + # The above lookup takes about 1ms, this one takes about 400ms + $service = Get-CIMInstance win32_service | Where-Object { ($_.PathName -match $servicePath) -and ($IncludeDisabled.isPresent -or ($_.StartMode -ne "Disabled"))} | Select-Object -First 1 + if (!$service) { + Write-Warning "$logLead : Chocolatey service $ChocolateyName could not be found!" + return $null + } + return $service +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServiceInfoByCIMFragment.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceInfoByCIMFragment.ps1 new file mode 100644 index 0000000..577ccce --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceInfoByCIMFragment.ps1 @@ -0,0 +1,68 @@ +Function Get-ServiceInfoByCIMFragment { +<# +.SYNOPSIS + Get the services of all services matched by the CIM fragment passed in. + +.PARAMETER QueryFragment + A name or path fragment to match against + +.PARAMETER ForceExact + Don't look for something "like" the QueryFragment, but _only_ this value + +.OUTPUTS + Will return an array of any services found +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [Alias("Fragment")] + [string]$QueryFragment = "", + + [switch]$ForceExact + ) + + $logLead = (Get-LogLeadName) + + # I want to ensure that anything with a slash is escaped if present + # However, if it came in double escaped already I just quadded it + # This is the lazier version of inspecting all \\ vs \ in the string. + $pathifiedFragment = $QueryFragment.Replace('\','\\').Replace('\\\\','\\') + + Write-Verbose "$logLead : Looking for anything that matches [$pathifiedFragment]" + + $filter = "PathName like '%{0}%' OR Name like '%{0}%'" -f $pathifiedFragment + if ($ForceExact) { + $filter = "PathName like '{0}' OR Name like '{0}'" -f $pathifiedFragment + } + + $cimServices = (Get-CIMInstance Win32_Service -Filter $filter) + + $returnResults = @() + + foreach($cimService in $cimServices) { + Write-Verbose "$logLead : Found [$($cimService.Name)] at [$($cimService.Path)]" + $rawExePath = ($cimService.PathName.Remove($cimService.PathName.LastIndexOf(".exe")) + ".exe").Replace('"', '') + + $returnResults += @{ + Name = $cimService.Name + DisplayName = $cimService.DisplayName + Path = $cimService.PathName + ExePath = $rawExePath + # SRE-18952 - We may be breaking things we don't expect - cbrand 2023-01-11 + # This is due to how we need to do better path matching for HTTP service names + ParentFolder = Split-Path -Path $rawExePath -Parent + Started = $cimService.Started + State = $cimService.State + Status = $cimService.Status + StartMode = $cimService.StartMode + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + StartType = $cimService.StartMode + ProcessId = $cimService.ProcessId + InstallDate = $cimService.InstallDate + UserName = $cimService.StartName + } + } + + return $returnResults +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServiceNamesByFragment.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceNamesByFragment.ps1 new file mode 100644 index 0000000..ad21392 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceNamesByFragment.ps1 @@ -0,0 +1,38 @@ +function Get-ServiceNamesByFragment { +<# +.SYNOPSIS + Get the service names of all services matched by the fragment passed in. + +.PARAMETER Name + A name fragment to match against + +.PARAMETER ReturnNullIfNotFound + Used to alter legacy behavior + +.OUTPUTS + Will return the all service names that match +#> + [CmdletBinding()] + [OutputType([System.String])] + Param( + [Parameter(Mandatory=$false)] + [Alias("Fragment")] + [string]$Name = "", + [switch]$ReturnNullIfNotFound + ) + + $logLead = (Get-LogLeadName) + + Write-Verbose ("$logLead : Checking for service names") + + $services = @((Get-Service) | Where-Object { (($_.DisplayName -match $Name) -or ($_.Name -match $Name)) }) + + if (Test-IsCollectionNullOrEmpty $services) { + if ($ReturnNullIfNotFound) { + return $null + } + return 'Not Installed' + } + + return ($services).ServiceName +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServiceStartupFailuresFromEventLog.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceStartupFailuresFromEventLog.ps1 new file mode 100644 index 0000000..4843458 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServiceStartupFailuresFromEventLog.ps1 @@ -0,0 +1,77 @@ +function Get-ServiceStartupFailuresFromEventLog { +<# +.SYNOPSIS + Get the (recent) service startup failures from the event log + Defaults to the past 12 hours + +.PARAMETER Since + Specify a time to search from. See also -Until + +.PARAMETER Until + Specify a time to search to. Requires -Since + +.PARAMETER LastHours + Specify a number of most recent hours to search for. Defaults to the last 12 hours + +.PARAMETER ServiceName + Specify a service name fragment to search for. + +.PARAMETER Readable + [switch] Produce slightly more parseable output at the cost of record details +#> + [CmdletBinding(DefaultParameterSetName = 'LastHours')] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Since')] + [System.DateTime]$Since, + [Parameter(Mandatory = $false, ParameterSetName = 'Since')] + [System.DateTime]$Until, + [Parameter(Mandatory = $false, ParameterSetName = 'LastHours')] + [ValidateScript({if ($_ -ne 0) { $true } else {throw "0 is invalid, please specify a number of hours to search since"}})] + [int]$LastHours = 12, + [Parameter(Mandatory = $false)] + [Alias('Message')] + [Alias('match')] + [Alias('Contains')] + [string]$ServiceName = '', + [Parameter(Mandatory = $false)] + [switch]$Readable + ) + + $logLead = Get-LogLeadName + + # 10,000 in this case is a magic string with no real value chosen behind it + # "a very large number" + if ((Get-WinEvent -ListLog Application).RecordCount -gt 10000) { + Write-Host "$logLead : This process takes a while to return all the records depending on how many are in the event log" + } + + if ($PSCmdlet.ParameterSetName -eq 'LastHours') { + if ($LastHours -gt 0) { + $LastHours = $LastHours * -1 + } + $Since = [System.DateTime]::Now.AddHours($LastHours) + } + + $splat = @{ + StartTime = $Since + LogName = 'Application' + ProviderName = 'Application Error' + Id = 1000 # magic number + } + + if ($null -ne $Until) { + $splat.EndTime = $Until + } + + $records = (Get-WinEvent -FilterHashtable $splat) + if (-not (Test-StringIsNullOrWhitespace($ServiceName))) { + $records = $records | Where-Object { $_.Properties.Value -match $ServiceName -or $_.Message -match $ServiceName } + } + + if ($Readable) { + $records | Format-Table -Property TimeCreated,Message -Wrap + } else { + return $records + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.ps1 new file mode 100644 index 0000000..fb68ac8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.ps1 @@ -0,0 +1,67 @@ +function Get-ServicesToStart { + +<# +.SYNOPSIS + Returns service names for legacy and Chocolatey services, plus filebeats, which need to be started. + +.DESCRIPTION + Returns service names for legacy and Chocolatey services, plus filebeats, which need to be started. Can optionally skip Legacy or Chocolatey services. Filebeats is always returned. + +.PARAMETER SkipChocolateyServices + [switch] Skips returning Chocolatey services + +.PARAMETER SkipAlkamiServices + [switch] Skips returning Alkami services + +.EXAMPLE +Get-ServicesToStart + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. +Alkami Radium Scheduler Service +Alkami.MicroServices.Authorization.Service.Host +Alkami.MicroServices.Features.Beacon.Host +Filebeat (Haystack) + +.EXAMPLE +Get-ServicesToStart -SkipChocolateyServices + +Alkami Radium Scheduler Service +Alkami.MicroServices.Authorization.Service.Host +Alkami.MicroServices.Features.Beacon.Host +Filebeat (Haystack) + +.EXAMPLE +Get-ServicesToStart -SkipAlkamiServices + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. +Alkami.MicroServices.Authorization.Service.Host +Alkami.MicroServices.Features.Beacon.Host +Filebeat (Haystack) +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [switch]$SkipChocolateyServices, + [Parameter(Mandatory=$false)] + [switch]$SkipAlkamiServices + ) + + $servicesTostart = @() + if (!($skipAlkamiServices.IsPresent)) + { + $alkamiServices = @(Get-AlkamiServices) + $legacyServicesToStart += @($alkamiServices | Where-Object { $_.Status -eq "Stopped" -and $_.Name -notmatch "Subscriptions" -and $_.Name -notmatch "Broker" }) + $servicesToStart += $legacyServicesToStart | ForEach-Object { $_.Name } + } + + if (!$skipChocolateyServices.IsPresent) + { + $chocolateyServices = @(Get-ChocolateyServices | Where-Object { $_.State -eq "Stopped" -and $_.Name -notmatch "Subscriptions" }) + $servicesToStart += $chocolateyServices | ForEach-Object {$_.Name} + } + + return $servicesToStart | Select-Object -Unique +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.tests.ps1 new file mode 100644 index 0000000..05dd3b5 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStart.tests.ps1 @@ -0,0 +1,65 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-ServicesToStart + +Describe "Get-ServicesToStart" { + + Mock -ModuleName $moduleForMock Get-AlkamiServices { return @( + @{ Status = "Running"; Name = "Subscriptions"; DisplayName = "Subscriptions Service"}, + @{ Status = "Running"; Name = "Alkami.MicroServices.Broker.Host"; DisplayName = "Alkami.MicroServices.Broker.Host"}, + @{ Status = "Running"; Name = "Alkami Nag Service"; DisplayName = "Alkami Nag Service"}, + @{ Status = "Stopped"; Name = "Alkami.Stopped.Service"; DisplayName = "Alkami Stopped Service"} + @{ Status = "Stopped"; Name = "Alkami.Stopped.Legacy.Service"; DisplayName = "Alkami Stopped Service"} + ) } + + Mock -ModuleName $moduleForMock Get-ChocolateyServices { return @( + @{ State = "Running"; Name = "Alkami.Services.Subscriptions.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.Subscriptions.Host`" -servicename `"Alkami.Services.Subscriptions.Host" }, + @{ State = "Running"; Name = "Alkami.MicroServices.Contacts.Service.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.Contacts.Service.Host`" -servicename `"Alkami.Services.Contacts.Service.Host" }, + @{ State = "Running"; Name = "Alkami.MicroServices.SiteText.Service.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.SiteText.Service.Host`" -servicename `"Alkami.Services.SiteText.Service.Host" }, + @{ State = "Stopped"; Name = "Alkami.Stopped.Service"; DisplayName = "Alkami Stopped Service"}, + @{ State = "Stopped"; Name = "Alkami.Stopped.Chocolatey.Service"; PathName = "`"C:\`" -displayname `"Alkami.Stopped.Service`" -servicename `"Alkami.Stopped.Service" } + ) } + + Context "Switch Validations" { + + It "Should skip AlkamiServices When switch Set" { + + Get-ServicesToStart -skipAlkamiServices + Assert-MockCalled -ModuleName $moduleForMock Get-AlkamiServices -Times 0 -Exactly -Scope It + } + + It "Should skip chocolatey service when switch set" { + + Get-ServicesToStart -skipChocolateyServices + Assert-MockCalled -ModuleName $moduleForMock Get-ChocolateyServices -Times 0 -Exactly -Scope It + } + + It "Should not return services that are already started" { + + $result = Get-ServicesToStart + $result.Count | Should -Be 3 + } + + It "Returns Only Unique Service Names" { + + Mock -ModuleName $moduleForMock Get-AlkamiServices { return @( + @{ Status = "Stopped"; Name = "Alkami.Duplicate.Service"; DisplayName = "Alkami Duplicate Service"} + ) } + + Mock -ModuleName $moduleForMock Get-ChocolateyServices { return @( + @{ State = "Stopped"; Name = "Alkami.Duplicate.Service"; DisplayName = "Alkami Duplicate Service"} + ) } + + $result = Get-ServicesToStart + $result.Count | Should -Be 1 + } + } + } + +#endregion Get-ServicesToStart \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.ps1 new file mode 100644 index 0000000..674eace --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.ps1 @@ -0,0 +1,69 @@ +function Get-ServicesToStop { +<# +.SYNOPSIS + Get the list of service names to be stopped. Focuses on chocolatey installed and Alkami named services (including Radium, Nag, etc) + +.PARAMETER SkipSubscriptionService + Should include the subscription service? + +.PARAMETER SkipChocolateyServices + Should include all services installed in the package library location? This may include non-Alkami services (think SDK, but also think FileBeats) + +.PARAMETER SkipAlkamiServices + Should include all Alkami services? This may include services not in the package library location + +.OUTPUTS + Returns the list of service names to be stopped +#> + [CmdletBinding()] + [OutputType([string[]])] + Param( + [Parameter(Mandatory = $false)] + [switch]$SkipSubscriptionService, + + [Parameter(Mandatory = $false)] + [switch]$SkipChocolateyServices, + + [Parameter(Mandatory = $false)] + [switch]$SkipAlkamiServices + ) + + $servicesToStop = @() + + if (!$SkipChocolateyServices) { + $rawservicesToStop = @(Get-ChocolateyServices | Where-Object { ($_.State -in ("Running", "StopPending") -and $_.Name -notmatch "Subscriptions") }) + + if (!(Test-IsCollectionNullOrEmpty $rawservicesToStop)) { + $servicesToStop += $rawservicesToStop.Name + } + } + + if (!$SkipAlkamiServices) { + $rawservicesToStop = @(Get-AlkamiServices) | Where-Object { $_.Status -in ("Running", "StopPending") } + + $legacyServicesToStop = @() + + $legacyServicesToStop += @($rawservicesToStop | Where-Object { $_.Name -notmatch "Subscriptions" }) + + #Add Subscription Service Last so it is the last one to be stopped + if (!$SkipSubscriptionService) { + $legacyServicesToStop += @($rawservicesToStop | Where-Object { $_.Name -match "Subscriptions" }) + } + + if (!(Test-IsCollectionNullOrEmpty $legacyServicesToStop)) { + $servicesToStop += $legacyServicesToStop.Name + } + } + + $filebeatServices = Get-FileBeatsService -ErrorAction SilentlyContinue + + if (!(Test-IsCollectionNullOrEmpty $filebeatServices)) { + foreach($service in $filebeatServices) { + if ($service.State -in ("Running", "StopPending")) { + $servicesToStop += $service.Name; + } + } + } + + return $servicesToStop | Select-Object -Unique +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.tests.ps1 new file mode 100644 index 0000000..99f2725 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-ServicesToStop.tests.ps1 @@ -0,0 +1,67 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Get-ServicesToStop + +Describe "Get-ServicesToStop" { + Mock -ModuleName $moduleForMock Get-FileBeatsService -MockWith {} + Mock -ModuleName $moduleForMock Get-AlkamiServices { return @( + @{ Status = "Running"; Name = "Subscriptions"; DisplayName = "Subscriptions Service"}, + @{ Status = "Running"; Name = "Alkami.MicroServices.Broker.Host"; DisplayName = "Alkami.MicroServices.Broker.Host"}, + @{ Status = "Running"; Name = "Alkami Nag Service"; DisplayName = "Alkami Nag Service"}, + @{ Status = "Stopped"; Name = "Alkami Stopped Service"; DisplayName = "Alkami Stopped Service"} + ) } + + Mock -ModuleName $moduleForMock Get-ChocolateyServices { return @( + @{ State = "Running"; Name = "Alkami.Services.Subscriptions.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.Subscriptions.Host`" -servicename `"Alkami.Services.Subscriptions.Host" }, + @{ State = "Running"; Name = "Alkami.MicroServices.Contacts.Service.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.Contacts.Service.Host`" -servicename `"Alkami.Services.Contacts.Service.Host" }, + @{ State = "Running"; Name = "Alkami.MicroServices.SiteText.Service.Host"; PathName = "`"C:\`" -displayname `"Alkami.Services.SiteText.Service.Host`" -servicename `"Alkami.Services.SiteText.Service.Host" }, + @{ State = "Stopped"; Name = "Alkami.Stopped.Service"; PathName = "`"C:\`" -displayname `"Alkami.Stopped.Service`" -servicename `"Alkami.Stopped.Service" } + ) } + + Context "Switch Validations" { + It "Should skip AlkamiServices When switch Set" { + + Get-ServicesToStop -skipAlkamiServices + + Assert-MockCalled -ModuleName $moduleForMock Get-AlkamiServices -Times 0 -Exactly -Scope It + } + + It "Should skip Subscription Service when switch set" { + $result = Get-ServicesToStop -skipSubscriptionService + + $result | Where-Object { $_ -match "Subscriptions" } | Should Be $null + } + + It "Should skip chocolatey service when switch set" { + Get-ServicesToStop -skipChocolateyServices + + Assert-MockCalled -ModuleName $moduleForMock Get-ChocolateyServices -Times 0 -Exactly -Scope It + } + + It "Returns the subscription services when switch not set" { + $result = Get-ServicesToStop + + $result | Where-Object { $_ -match "Subscriptions" } | Should Not Be $null + } + + It "Returns the event broker when switch not set" { + $result = Get-ServicesToStop + + $result | Where-Object { $_ -match "Alkami.MicroServices.Broker.Host" } | Should Not Be $null + } + + It "Should not return services that are already stopped" { + $result = Get-ServicesToStop + + $result | Where-Object { $_ -match "Stopped" } | Should Be $null + } + } +} + +#endregion Get-ServicesToStop \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.ps1 new file mode 100644 index 0000000..844f48e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.ps1 @@ -0,0 +1,36 @@ +function Get-WindowsServiceApplicationName { +<# +.SYNOPSIS + Gets the full file path to a Windows service's application executable +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$serviceName + ) + + $logLead = (Get-LogLeadName) + + Write-Host "$logLead : Querying WMI for ServiceInformation for $serviceName" + $serviceDetail = Get-CIMInstance win32_service -Filter "Name like '%$serviceName%'" + + if ($null -eq $serviceDetail) { + + Write-Warning "$logLead : Unable to find service detail for service with name $serviceName" + return $null + + } elseif ($null -eq $serviceDetail.PathName) { + + # This should never happen. Probably. + Write-Warning "$logLead : Unable to find the property PathName for service with name $serviceName" + return $null + } elseif ($serviceDetail.Count -gt 1) { + + # This shouldn't happen either if you're using it right. + Write-Warning "$logLead : More than one service found using service name filter $serviceName. Check the input parameter!" + #return $null + } + + return ($serviceDetail | Select-Object -First 1 | Select-Object -ExpandProperty PathName) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.tests.ps1 new file mode 100644 index 0000000..e814995 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationName.tests.ps1 @@ -0,0 +1,79 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-WindowsServiceApplicationName" { + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + $fakeServiceName = -join ((65..90) + (97..122) | Get-Random -Count 15 | ForEach-Object {[char]$_}) + + Context "Error Handling" { + + It "Does not throw if the service is not found" { + + { Get-WindowsServiceApplicationName "$fakeServiceName" } | Should -Not -Throw + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Unable to find service detail" } + } + + It "Does not throw if the service has no pathname property" { + + Mock -CommandName Get-CIMInstance -MockWith { + + return New-Object PSObject -Property @{ NoPathName="Here"; } + } -ModuleName $moduleForMock + + { Get-WindowsServiceApplicationName "$fakeServiceName" } | Should -Not -Throw + Assert-MockCalled -CommandName Get-CIMInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Unable to find the property PathName" } + } + + It "Returns one service if more than one matching service is found" { + + Mock -CommandName Get-CIMInstance -MockWith { + + @( + [PSCustomObject]@{ + Name = "FakeService"; + PathName = "NoWay"; + }, + [PSCustomObject]@{ + Name = "FakeService2"; + PathName = "NoHow"; + } + ) + + } -ModuleName $moduleForMock + + Get-WindowsServiceApplicationName "$fakeServiceName" | Should -HaveCount 1 + + Assert-MockCalled -CommandName Get-CIMInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "More than one service found" } + } + } + + Context "Happy Path" { + + It "Returns only the PathName property" { + + Mock -CommandName Get-CIMInstance -MockWith { + + [PSCustomObject]@{ + Name = "FakeService"; + PathName = "HappyPath"; + } + + } -ModuleName $moduleForMock + + Get-WindowsServiceApplicationName "$fakeServiceName" | Should -Be "HappyPath" + Assert-MockCalled -CommandName Get-CIMInstance -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.ps1 new file mode 100644 index 0000000..d8c1deb --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.ps1 @@ -0,0 +1,53 @@ +function Get-WindowsServiceApplicationPath { +<# +.SYNOPSIS + Gets the folder path to a Windows service's application executable + +.PARAMETER ServiceName + Name of the service to get the path to the folder that contains the exe + +.NOTES + Previously this function called another function that would automagically select + the first result if there were more than one matching service name. This could be + semi-random and non-useful. Instead of continuing that, we will not act on a + non-deterministic or non-specific result. +#> + + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) + + $logLead = Get-LogLeadName + + # Using Get-ServiceInfoByCIMFragment to keep the Exe-trimming to one place + $serviceInfo = Get-ServiceInfoByCIMFragment -QueryFragment $serviceName + + if ($null -eq $serviceInfo) { + + Write-Warning "$logLead : Unable to locate service info" + return $null + } + + # Get-ServiceInfoByCIMFragment returns an array of hashtables, unless... + # only one service is found, and then the array is magically unboxed to a hashtable + # either way, directly accessing the ExePath lets us check the count of ExePaths + # More than one is not useful, or actionable, because we cannot predict what it + # will be + if ($service.ExePath.Count -gt 1) { + + Write-Warning "$logLead : More than one service match the name $serviceName" + Write-Warning "$logLead : Use a more precise name" + return $null + } + + $fullAppPath = $serviceInfo.ExePath + Write-Host "$logLead : Application Path is $fullAppPath" + + $directoryOnly = Split-Path -Path $fullAppPath -Parent + Write-Host "$logLead : Directory is $directoryOnly" + + return $directoryOnly +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.tests.ps1 new file mode 100644 index 0000000..83dfe39 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Get-WindowsServiceApplicationPath.tests.ps1 @@ -0,0 +1,92 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Get-WindowsServiceApplicationPath" { + + $fakeServiceNormal = "SvcNormalPath" + $expectedPathNormal = "TestDrive:\TestPath" + $fakeServiceSpace = "SvcSpacePath" + $expectedPathWithSpace = "TestDrive:\TestPath WithSpace" + $fakeServiceName = -join ((65..90) + (97..122) | Get-Random -Count 15 | ForEach-Object { [char]$_ }) + + Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Verbose -MockWith {} -ModuleName $moduleForMock + Mock -CommandName Write-Error -MockWith {} -ModuleName $moduleForMock + + Mock -CommandName Get-ServiceInfoByCIMFragment -ModuleName $moduleForMock -MockWith { + $exeName = "$($QueryFragment).exe" + $returnPath = Join-Path -Path $expectedPathNormal -ChildPath $exeName + $returnResults = @() + $returnResults += @{ + #Name = $cimService.Name + #DisplayName = $cimService.DisplayName + #Path = $cimService.PathName + ExePath = $returnPath#($cimService.PathName.Remove($cimService.PathName.LastIndexOf(".exe")) + ".exe").Replace('"', '') + #Started = $cimService.Started + #State = $cimService.State + #Status = $cimService.Status + #StartMode = $cimService.StartMode + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + #StartType = $cimService.StartMode + #ProcessId = $cimService.ProcessId + #InstallDate = $cimService.InstallDate + #UserName = $cimService.StartName + } + return $returnResults + } -ParameterFilter { $QueryFragment -eq "SvcNormalPath" } + + Mock -CommandName Get-ServiceInfoByCIMFragment -ModuleName $moduleForMock -MockWith { + $exeName = "$($QueryFragment).exe" + $returnPath = Join-Path -Path $expectedPathWithSpace -ChildPath $exeName + $returnResults = @() + $returnResults += @{ + #Name = $cimService.Name + #DisplayName = $cimService.DisplayName + #Path = $cimService.PathName + ExePath = $returnPath#($cimService.PathName.Remove($cimService.PathName.LastIndexOf(".exe")) + ".exe").Replace('"', '') + #Started = $cimService.Started + #State = $cimService.State + #Status = $cimService.Status + #StartMode = $cimService.StartMode + # Provided to make comparison checks to other things easier. + # The name in several places is StartType + #StartType = $cimService.StartMode + #ProcessId = $cimService.ProcessId + #InstallDate = $cimService.InstallDate + #UserName = $cimService.StartName + } + return $returnResults + } -ParameterFilter { $QueryFragment -eq "SvcSpacePath" } + + Context "Error Handling" { + + It "Does not throw if the service is not found" { + + { Get-WindowsServiceApplicationPath "$fakeServiceName" } | Should -Not -Throw + Assert-MockCalled -CommandName Write-Warning -ParameterFilter { $Message -match "Unable to locate service info" } -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + } + + Context "Service Executable Formatting" { + + It "Handles_Paths_With_Spaces" { + + Get-WindowsServiceApplicationPath -serviceName $fakeServiceSpace | Should -Be $expectedPathWithSpace + Assert-MockCalled -CommandName Get-ServiceInfoByCIMFragment -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Handles_Paths_Without_Spaces" { + + Get-WindowsServiceApplicationPath -serviceName $fakeServiceNormal | Should -Be $expectedPathNormal + Assert-MockCalled -CommandName Get-ServiceInfoByCIMFragment -Times 1 -Exactly -Scope It -ModuleName $moduleForMock + } + + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Grant-UserLogonAsServiceRights.ps1 b/Modules/Alkami.PowerShell.Services/Public/Grant-UserLogonAsServiceRights.ps1 new file mode 100644 index 0000000..760eb4c --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Grant-UserLogonAsServiceRights.ps1 @@ -0,0 +1,19 @@ +function Grant-UserLogonAsServiceRights { +<# +.SYNOPSIS + Grants a User the Logon as a Service Right +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$userName + ) + + $logLead = (Get-LogLeadName); + Grant-UserLocalSecurityPolicyRights $userName "SeServiceLogonRight" + + $secPolLogContent = Get-Content (Join-Path $env:windir "security\logs\scesrv.log") + $secPolLogContent | ForEach-Object { Write-Verbose ("$logLead : {0}" -f $_)} +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Grant-UserStartStopRightsToService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Grant-UserStartStopRightsToService.ps1 new file mode 100644 index 0000000..a7a487b --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Grant-UserStartStopRightsToService.ps1 @@ -0,0 +1,42 @@ +function Grant-UserStartStopRightsToService { +<# +.SYNOPSIS + Grants a non-administrative user rights to stop or start a Windows Service +#> + + [CmdLetBinding()] + Param( + [Parameter(Mandatory=$true)] + [Alias("User")] + [string]$userName, + + [Parameter(Mandatory=$false)] + [Alias("Domain")] + [string]$domainName, + + [Parameter(Mandatory=$true)] + [Alias("Service")] + [string]$serviceName + ) + + $logLead = (Get-LogLeadName); + $serviceAcls = & sc.exe sdshow "$serviceName" + $userSid = Get-SidFromUsername -userName:$userName -domainName:$domainName + + if ($serviceAcls -match "$userSid") + { + Write-Warning ("$logLead : User {0} already has explicit rights to the service. Verify they are correct and remove manually if this needs to be rerun." -f $userName) + return + } + + $splitAcls = ($serviceAcls -split "(?=S:\(AU)" -ne "") + $aclTemplate = ("(A;;RPWPCR;;;{0})" -f $userSid) + Write-Verbose ("$logLead : ACL String to Add: {0}" -f $aclTemplate) + + $modifiedAclSegment = $splitAcls[0] + $aclTemplate + $modifiedAcls = $modifiedAclSegment + ($splitAcls | Select-Object -Skip 1) + Write-Verbose ("$logLead : Setting ACLs for Service {0} to {1}" -f $serviceName, $modifiedAcls) + + & sc.exe sdset "$serviceName" $modifiedAcls +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Install-AlkamiService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Install-AlkamiService.ps1 new file mode 100644 index 0000000..1047181 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Install-AlkamiService.ps1 @@ -0,0 +1,351 @@ +function Install-AlkamiService { +<# +.SYNOPSIS + Installs an Alkami Windows Service + +.DESCRIPTION + Installs an Alkami Windows Service. + Services installed via this function are not installed to a Service Fabric or other mesh implementation. + This installer does not create k8s or Lambdas or anything that is not a Windows Service. + + If the service is found under the specified name and path already registered, this function will exit early and do nothing. + +.PARAMETER ServicePath + The path to the service folder or service file. If to a folder, assumes that the file matches the path name. + Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path + +.PARAMETER AssemblyInfo + The expected name of the service. This overrides the default option of matching the path ID. + +.PARAMETER DisplayName + The expected display name of the service. + +.PARAMETER IsDatabaseAccessRequired + Does this need to access a database? + +.PARAMETER StartOnInstall + Should the program start on install? Defaults to true, must be used to not-start on install + +.PARAMETER StartTimeout + The StartTimeout to be passed to Start-AlkamiService. Defaults to 60 + +.PARAMETER SetNewRelicConfiguration + Should the New Relic configuration be applied to this service? + +.PARAMETER StartType + Options are "Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart" + See also the switches: StartDelayed, StartDisabled, StartAutomatically + +.EXAMPLE + Install-AlkamiService C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host +#> + [CmdletBinding(DefaultParameterSetName = 'StartSpecified')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Scope='Function', Justification='Negated-action switch names are non-intuitive. It should take hoops to disable.')] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [Alias("Path")] + [string]$ServicePath, + [Parameter(Mandatory = $false, Position = 1)] + [Alias("Name")] + [string]$AssemblyInfo, + [string]$DisplayName, + [Alias("UseDBUser")] + [switch]$IsDatabaseAccessRequired, + # Set to true for the dev experience. Expect the pipeline to set to false. + [switch]$StartOnInstall = $true, + [int]$StartTimeout = 60, + [switch]$SetNewRelicConfiguration, + + [Parameter(Mandatory = $false, Position = 2)] + [Parameter(ParameterSetName = "StartSpecified")] + [ValidateSet("Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart","")] + [string]$StartType = "", + [Alias("AutoStart")] + [Parameter(ParameterSetName = "StartAutomatically")] + [switch]$StartAutomatically, + [Alias("Disabled")] + [Parameter(ParameterSetName = "StartDisabled")] + [switch]$StartDisabled, + [Alias("Manual")] + [Parameter(ParameterSetName = "StartManual")] + [switch]$StartManual, + [Alias("Delayed")] + [Parameter(ParameterSetName = "StartDelayed")] + [switch]$StartDelayed + ) + + $delayedParameterConstant = "delayed-auto" + $delayedConstant = "AutomaticDelayedStart" + $disabledParameterConstant = "disabled" + $disabledConstant = "Disabled" + $manualParameterConstant = "demand" + $manualConstant = "Manual" + $automaticParameterConstant = "autostart" + $automaticConstant = "Automatic" + + $logLead = Get-LogLeadName + + $tier0Services = Get-ServicesByTier -Tier 0 + + $StartOnInstallConfigSetting = (Get-ConfigSetting "Alkami.Installer.StartOnInstall") + if (![string]::IsNullOrWhiteSpace($StartOnInstallConfigSetting) -and ([bool]::TryParse($StartOnInstallConfigSetting,[ref]$StartOnInstallConfigSetting))) { + if ($StartOnInstall -ne $StartOnInstallConfigSetting) { + Write-Warning "$logLead : StartOnInstall flag conflicts with machine config [$StartOnInstallConfigSetting]" + } + $StartOnInstall = $StartOnInstallConfigSetting + } + + # Tier 0 is hard coded to Automatic - Delayed -- SRE-17490 + if($tier0Services -Contains $AssemblyInfo ){ + $StartType = $delayedConstant + # Skip these checks for tier0 + } else { + if ([string]::IsNullOrWhiteSpace($StartType)) { + $serviceStartupModeAppSettingKey = "Alkami.Installer.ServiceStartupMode" + $defaultInstallerMode = Get-AppSetting $serviceStartupModeAppSettingKey + if (![string]::IsNullOrWhiteSpace($defaultInstallerMode)) { + if ([string]::Equals($defaultInstallerMode,"OFF",[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $disabledConstant + } elseif ([string]::Equals($defaultInstallerMode,"ON",[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $manualConstant + } elseif ([string]::Equals($defaultInstallerMode,$automaticConstant,[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $automaticConstant + } else { + Write-Warning "$logLead : Can not parse appSetting [$serviceStartupModeAppSettingKey] with value [$defaultInstallerMode] (acceptable: on, off, $automaticConstant)" + } + } + } + + if ([string]::IsNullOrWhiteSpace($StartType)) { + $StartType = $manualConstant + } + + # Allow the user to type either or on AutomaticDelayedStart or Disabled + # The value here is used in this lookup sequence and later with Set-Service -StartupType + if ($StartDelayed -or ($StartType -eq "Delayed")) { + $StartType = $delayedConstant + } + + if ($StartAutomatically) { + $StartType = $automaticConstant + } + + if ($StartDisabled) { + $StartType = $disabledConstant + } + + if ($StartManual) { + $StartType = $manualConstant + } + } + + # Hash lookup - quicker than a switch + $serviceInstallType = @{ + Manual = $manualParameterConstant; + Disabled = $disabledParameterConstant; + Automatic = $automaticParameterConstant; + AutomaticDelayedStart = $delayedParameterConstant; + }.$StartType + +<# +# This is gated behind the pipeline, so we have no need to test right now. + if (!(Test-IsDeveloperMachine) -and (Test-IsWebServer)) { + Write-Warning "$loglead : Can not install microservices on the web tier" + return + } +#> + if (!(Test-Path $ServicePath)) { + Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not install something which does not exist." + return + } + + # Ensure it doesn't end with exe (yet) + if ($AssemblyInfo.EndsWith(".exe")) { + $AssemblyInfo = $AssemblyInfo.Remove($AssemblyInfo.LastIndexOf(".exe")) + } + + $ServicePath = (Resolve-Path $ServicePath) + $folderPath = $ServicePath + $serviceAlreadyExists = $false + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + $serviceAccount = (Get-MachineConfigServiceAccount -IsDatabaseAccessRequired:$IsDatabaseAccessRequired) + + $item = (Get-Item $ServicePath) + if ($item.PSIsContainer) { + # path was a folder + $leafName = (Split-Path -Path $ServicePath -Leaf) + if ($leafName -match 'tools') { + $ServicePath = (Split-Path -Path $ServicePath -Parent) + $leafName = (Split-Path -Path $ServicePath -Leaf) + } + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = $leafName + } + + $exeName = "$AssemblyInfo.exe" + + Write-Host "$logLead : looking for [$exeName] in [$ServicePath]" + + $exeItems = @() + # Start with @("app","lib","tools") then do the naive scan + foreach($subpath in @("app","lib","tools")) { + $expectedSubpath = Join-Path -Path $ServicePath -ChildPath $subpath + $expectedSubpathExe = Join-Path -Path $expectedSubpath -ChildPath $exeName + if (Test-Path -Path $expectedSubpathExe) + { + $exeItems = @(Get-Item -Path $expectedSubpathExe) + } + else { + Write-Host "Tried to find an item in a subpath [$expectedSubpath] that doesn't exist. This is fine." + } + + if (-not (Test-IsCollectionNullOrEmpty -Collection $exeItems)) { + Write-Host "Found an exe in [$expectedSubpath] at [$expectedSubpathExe]" + break; + } + } + + # If we still didn't find anything, fall back to the "naive" pattern + if (Test-IsCollectionNullOrEmpty -Collection $exeItems) { + Write-Host "$logLead : Looking for files via naive-lookup path" + $exeItems = Get-ChildItem -Path (Join-Path $ServicePath $exeName) -Recurse + } + + # If we are in naive and find the same .exe multiple times, throw + if ($exeItems.Count -gt 1) { + $additionalDetailMessage = ", can not continue as we don't know which to use" + if ($ErrorActionPreference -ne 'Stop') { + $additionalDetailMessage = ", going to try to install the first one." + } + + Write-Error "$logLead : More than one EXE available$additionalDetailMessage" + Write-Host "$logLead : $([string].Join(',',$exeItems.FullName))" + } + + $exeItem = $exeItems | Select-Object -First 1 # there should only be one exe with the [package name].exe inside the folders + + if ($null -eq $exeItem) { + $stopWatch.Stop() + Write-Warning "$logLead : Path passed in does not contain an exe with the filename: [$exeName]. Can not install a non-existent microservice in [$($stopWatch.Elapsed)]." + return + } + + $ServicePath = $exeItem.FullName + } else { + # The $item was not a .PSIsContainer so it was a file + # That means we want the folderPath to be the folder of the file + $folderPath = (Split-Path -Path $ServicePath -Parent) + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = (Split-Path -Path $ServicePath -Leaf).Replace(".exe","") + } + } + + $exeName = (Split-Path -Path $ServicePath -Leaf) + + $serviceAlreadyExists = $null -ne (Get-Service -Name $AssemblyInfo -ErrorAction Ignore) + + # If the service already existed we don't have to install it, but we still do the other things + if (!$serviceAlreadyExists) { + # Do the thing here to install the MS. + Write-Host "$logLead : Installing [$exeName] from $ServicePath" + + $argumentList = @() + + $argumentList += "create" + + # SRE-17892 - If given a display-name (which only happens in New-AppTierWindowsServices) use that for the "service name" + # This is useful for Nag and Radium as the service name does not match what is expected + if (![string]::IsNullOrWhiteSpace($DisplayName)) { + $argumentList += "$DisplayName" + # HARD PIVOT + # We are now using DisplayName for AssemblyInfo. Before it was used for service lookup by file only + # Because we are changing how it is registered in _selective_ cases, we are going to change + # the rest of the file on how we use it + Write-Host "$logLead : Changing Install-AlkamiService to use [$DisplayName] for `$AssemblyInfo in-script - This notice is for SRE benefit." + $AssemblyInfo = $DisplayName + } else { + $argumentList += "$AssemblyInfo" + } + $argumentList += "binpath=$ServicePath" + $argumentList += "start=$serviceInstallType" + + if (![string]::IsNullOrWhiteSpace($DisplayName)) { + $argumentList += "DisplayName=$DisplayName" + } + + Write-Host $argumentList + + Invoke-SCExe -Arguments $argumentList + Write-Host "$logLead : Successfully finished [$ServicePath] registration of service" + + Write-Host "$logLead : 150ms sleep for [$ServicePath] for flushing" + Start-Sleep -Milliseconds 150 + } else { + Stop-AlkamiService $AssemblyInfo + + $splat = @{ + ServiceName = $AssemblyInfo + StartupType = $StartType + } + + if (![string]::IsNullOrWhiteSpace($DisplayName)) { + $splat.DisplayName = $DisplayName + } + # Powershell 5.1 doesn't support using Set-Service to set the start type to Automatic-Delayed. So we do it with SC.exe + if($splat.StartupType -eq $delayedConstant){ + $params = @("config", $AssemblyInfo, "start=delayed-auto") + + Invoke-SCExe -Arguments $params + $splat.Remove('StartupType') + } + + # Call this regardless to set the DisplayName + Set-Service @splat + } + + if ($null -ne (Get-Service $AssemblyInfo)) { + Write-Host "$logLead : Attempt to Set-ChocolateyPackageNewRelicState" + $packageName = (Get-ChocoPackageFromPath $ServicePath) + + if (![string]::IsNullOrWhiteSpace($packageName)) { + Write-Verbose "$logLead : Set-ChocolateyPackageNewRelicState -Name `"$packageName`" -Enabled $SetNewRelicConfiguration" + Set-ChocolateyPackageNewRelicState -Name $packageName -Enabled $SetNewRelicConfiguration + } + } else { + throw "$loglead : Newly registered service could not be found." + } + + # We always call this, whether we just installed it or it already existed + # The reason for this is two-fold. + # 1. In case we are correcting a bad install. + # 2. Resets the gMSA "password already rolled" issue that we use "fixlogins" to resolve + # 2a. This wouldn't resolve "the service got installed 3 months ago" need for "fixlogins", just reinstall + Set-WindowsServiceExecutionAccount -ServiceName $AssemblyInfo -ServiceUser $serviceAccount -IsGMSAAccount + + Set-ServiceRecoveryOneRestart -ServiceName $AssemblyInfo + + if ($StartOnInstall) { + try { + Start-AlkamiService -ServiceName $AssemblyInfo -Timeout $StartTimeout + } catch { + Write-Warning "$logLead : Service could not be started. See logs above." + if ($serviceAlreadyExists) { + throw $_ + } else { + Write-Warning "$logLead : Continuing with install so files will be present post chocolatey install. This is not reporting as an error due to the way chocolatey works during installs, where if it fails it rolls back, but we've already registered the service with Windows. The alternative is to uninstall on failure and let the package be removed." + } + } + } else { + Write-Host "$logLead : Service not started, flag to start was not present or was not set to true" + } + + $stopWatch.Stop() + + Write-Host "$logLead : [$AssemblyInfo] installed at [$ServicePath] in [$($stopWatch.Elapsed)]" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Install-LegacyMicroservice.ps1 b/Modules/Alkami.PowerShell.Services/Public/Install-LegacyMicroservice.ps1 new file mode 100644 index 0000000..525405d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Install-LegacyMicroservice.ps1 @@ -0,0 +1,437 @@ +function Install-LegacyMicroservice { +<# +.SYNOPSIS + Installs a legacy Alkami Windows Microservice + +.DESCRIPTION + Installs a legacy Alkami Windows Microservice. + Legacy Microservices are considered to be Microservices that implement Alkami.Services.Subscriptions.ParticipatingService.DistributedServiceBase or derivatives. + Legacy Microservices installed via this function are not installed to a Service Fabric or other mesh implementation. + This installer does not create k8s or Lambdas or anything that is not a Windows Service. + + If the service is found under the specified name and path already registered, this function will exit early and do nothing. + +.PARAMETER ServicePath + The path to the service folder or service file. If to a folder, assumes that the file matches the path name. + Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path + +.PARAMETER AssemblyInfo + The expected name of the service. This overrides the default option of matching the path ID. + +.PARAMETER IsDatabaseAccessRequired + Does this need to access a database? + +.PARAMETER UseLegacyConfigForServiceName + Look up the legacy service name from the config.ps1 file. This is non-preferred. This is only used if the AssemblyInfo parameter is empty. + +.PARAMETER StartOnInstall + Should the program start on install? Defaults to true, must be used to not-start on install + +.PARAMETER StartTimeout + The StartTimeout to be passed to Start-AlkamiService. Defaults to 60 + +.PARAMETER SetNewRelicConfiguration + Should the New Relic configuration be applied to this service? + +.PARAMETER StartType + Options are "Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart" + See also the switches: StartDelayed, StartDisabled, StartAutomatically + +.EXAMPLE + Install-LegacyMicroservice C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host +#> + [CmdletBinding(DefaultParameterSetName = 'StartSpecified')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Scope='Function', Justification='Negated-action switch names are non-intuitive. It should take hoops to disable.')] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [Alias("Path")] + [string]$ServicePath, + [Parameter(Mandatory = $false, Position = 1)] + [Alias("Name")] + [string]$AssemblyInfo, + [Alias("UseDBUser")] + [switch]$IsDatabaseAccessRequired, + # Set to true for the dev experience. Expect the pipeline to set to false. + [switch]$StartOnInstall = $true, + [int]$StartTimeout = 60, + [switch]$SetNewRelicConfiguration, + [switch]$UseLegacyConfigForServiceName, + + [Parameter(Mandatory = $false, Position = 2)] + [Parameter(ParameterSetName = "StartSpecified")] + [ValidateSet("Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart","")] + [string]$StartType = "", + [Alias("AutoStart")] + [Parameter(ParameterSetName = "StartAutomatically")] + [switch]$StartAutomatically, + [Alias("Disabled")] + [Parameter(ParameterSetName = "StartDisabled")] + [switch]$StartDisabled, + [Alias("Manual")] + [Parameter(ParameterSetName = "StartManual")] + [switch]$StartManual, + [Alias("Delayed")] + [Parameter(ParameterSetName = "StartDelayed")] + [switch]$StartDelayed + ) + + $delayedParameterConstant = "--delayed" + $delayedConstant = "AutomaticDelayedStart" + $disabledParameterConstant = "--disabled" + $disabledConstant = "Disabled" + $manualParameterConstant = "--manual" + $manualConstant = "Manual" + $automaticParameterConstant = "--autostart" + $automaticConstant = "Automatic" + + $tier0Services = Get-ServicesByTier -Tier 0 + + # Invoke Command With Retry timing. + $icwrTiming = @{ + MaxRetries = 5 + Milliseconds = 5000 + JitterMin = 50 + JitterMax = 1000 + } + + $logLead = (Get-LogLeadName) + + $StartOnInstallConfigSetting = (Get-ConfigSetting "Alkami.Installer.StartOnInstall") + if (![string]::IsNullOrWhiteSpace($StartOnInstallConfigSetting) -and ([bool]::TryParse($StartOnInstallConfigSetting,[ref]$StartOnInstallConfigSetting))) { + if ($StartOnInstall -ne $StartOnInstallConfigSetting) { + Write-Warning "$logLead : StartOnInstall flag conflicts with machine config [$StartOnInstallConfigSetting]" + } + $StartOnInstall = $StartOnInstallConfigSetting + } + + # Tier 0 is hard coded to Automatic - Delayed -- SRE-17490 + if($tier0Services -Contains $AssemblyInfo){ + $StartType = $delayedConstant + # Skip these checks for tier0 + } else { + if ([string]::IsNullOrWhiteSpace($StartType)) { + $serviceStartupModeAppSettingKey = "Alkami.Installer.ServiceStartupMode" + $defaultInstallerMode = Get-AppSetting $serviceStartupModeAppSettingKey + if (![string]::IsNullOrWhiteSpace($defaultInstallerMode)) { + if ([string]::Equals($defaultInstallerMode,"OFF",[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $disabledConstant + } elseif ([string]::Equals($defaultInstallerMode,"ON",[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $manualConstant + } elseif ([string]::Equals($defaultInstallerMode,$automaticConstant,[StringComparison]::CurrentCultureIgnoreCase)){ + $StartType = $automaticConstant + } else { + Write-Warning "$logLead : Can not parse appSetting [$serviceStartupModeAppSettingKey] with value [$defaultInstallerMode] (acceptable: on, off, $automaticConstant)" + } + } + } + + if ([string]::IsNullOrWhiteSpace($StartType)) { + $StartType = $manualConstant + } + + # Allow the user to type either or on AutomaticDelayedStart or Disabled + # The value here is used in this lookup sequence and later with Set-Service -StartupType + if ($StartDelayed -or ($StartType -eq "Delayed")) { + $StartType = $delayedConstant + } + + if ($StartAutomatically) { + $StartType = $automaticConstant + } + + if ($StartDisabled) { + $StartType = $disabledConstant + } + + if ($StartManual) { + $StartType = $manualConstant + } + } + + # Hash lookup - quicker than a switch + $serviceInstallType = @{ + Manual = $manualParameterConstant; + Disabled = $disabledParameterConstant; + Automatic = $automaticParameterConstant; + AutomaticDelayedStart = $delayedParameterConstant; + }.$StartType + +<# +# This is gated behind the pipeline, so we have no need to test right now. + if (!(Test-IsDeveloperMachine) -and (Test-IsWebServer)) { + Write-Warning "$loglead : Can not install microservices on the web tier" + return + } +#> + if (!(Test-Path $ServicePath)) { + Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not install something which does not exist." + return + } + + # Ensure it doesn't end with exe (yet) + if ($AssemblyInfo.EndsWith(".exe")) { + $AssemblyInfo = $AssemblyInfo.Remove($AssemblyInfo.LastIndexOf(".exe")) + } + + $ServicePath = (Resolve-Path $ServicePath) + $folderPath = $ServicePath + $serviceAlreadyExists = $false + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + $serviceAccount = (Get-MachineConfigServiceAccount -IsDatabaseAccessRequired:$IsDatabaseAccessRequired) + + # If you passed in a value for AssemblyInfo then there is no reason to use the legacy config + # If you did not pass it in, but you did specify the flag, then on we go + # The parameter set names are already a bit lengthy, I'm trying to avoid changing that here + if ([string]::IsNullOrWhiteSpace($AssemblyInfo) -and $UseLegacyConfigForServiceName) { + Write-Verbose "$logLead : Trying to find the `$AssemblyInfo from the legacy config.ps1" + $configPs1s = (Get-ChildItem -Path $ServicePath -Filter "config.ps1" -Recurse) + + $serviceId = "" + + foreach($config in $configPs1s) { + $configFullName = $config.FullName + + $lines = (Get-Content $configFullName) + foreach($line in $lines) { + if ($line.Trim().ToLower().StartsWith('$serviceid')) { + $serviceId = ($line.Split('=')[1]).Replace(';','').Replace('"','').Replace("'","").Trim(); + } + } + + if (![string]::IsNullOrWhiteSpace($serviceId)) { + # Write out where we got it from in case we need to debug why we got this from a wrong location, etc + Write-Verbose "$logLead : Found [$serviceId] in [$configFullName]]" + break + } + } + + # Store the value we got back so we can use it in other places since AssemblyInfo was already empty when we got here + $AssemblyInfo = $serviceId + } + + $item = (Get-Item $ServicePath) + if ($item.PSIsContainer) { + # path was a folder + $leafName = (Split-Path -Path $ServicePath -Leaf) + if ($leafName -match 'tools') { + $ServicePath = (Split-Path -Path $ServicePath -Parent) + $leafName = (Split-Path -Path $ServicePath -Leaf) + } + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = $leafName + } + + $exeName = "$AssemblyInfo.exe" + + Write-Host "$logLead : looking for $exeName in $ServicePath" + + $exeItems = @() + # Start with @("app","lib","tools") then do the naive scan + foreach($subpath in @("app","lib","tools")) { + if(Test-Path(Join-Path (Join-Path $ServicePath $subpath) $exeName)) + { + $exeItems = @(Get-Item -Path (Join-Path (Join-Path $ServicePath $subpath) $exeName)) + } + else { + Write-Verbose "Tried to find an item in a subpath ($subpath) that doesn't exist. This is fine." + } + + if (!(Test-IsCollectionNullOrEmpty $exeItems)) { + Write-Verbose "Found an exe in $subpath" + break; + } + } + # If we still didn't find anything, fall back to the "naive" pattern + if (Test-IsCollectionNullOrEmpty $exeItems) { + $exeItems = (Get-ChildItem -Path (Join-Path $ServicePath $exeName) -Recurse) + } + # If we are in naive and find the same .exe multiple times, throw + if ($exeItems.Count -gt 1) { + $additionalDetailMessage = ", can not continue as we don't know which to use" + if ($ErrorActionPreference -ne 'Stop') { + $additionalDetailMessage = ", going to try to install the first one." + } + + Write-Error "$logLead : More than one EXE available$additionalDetailMessage" + Write-Host "$logLead : $([string].Join(',',$exeItems.FullName))" + } + + $exeItem = $exeItems | Select-Object -First 1 # there should only be one exe with the [package name].exe inside the folders + + if ($null -eq $exeItem) { + $stopWatch.Stop() + Write-Warning "$logLead : Path passed in does not contain an exe with the filename: [$exeName]. Can not install a non-existent microservice in [$($stopWatch.Elapsed)]." + return + } + $ServicePath = $exeItem.FullName + } else { + # The $item was not a .PSIsContainer so it was a file + # That means we want the folderPath to be the folder of the file + $folderPath = (Split-Path -Path $ServicePath -Parent) + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = (Split-Path -Path $ServicePath -Leaf).Replace(".exe","") + } + } + + # Check to see if the service is already registered before we try and do extra stuff + # This is where we could unregister it before we continue if we want to force a re-registration + $serviceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $AssemblyInfo) + + # If we didn't find anything by the assemblyinfo, let's double check the path in case something was already there + if (Test-IsCollectionNullOrEmpty $serviceCandidates) { + $serviceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $folderPath) + } + + # If we found something, log it and quit + if (!(Test-IsCollectionNullOrEmpty $serviceCandidates)) { + # Throw some comments into the console so we know why we didn't do anything + foreach ($serviceCandidate in $serviceCandidates) { + Write-Host "$logLead : Found an already existing service [$($serviceCandidate.Name)] at [$($serviceCandidate.Path)]" + + # The path could be complex. The beginning of the paths should match. + # The reason for complex paths has to do with how services are registered in the database + # It's possible to have flags at the end of the path etc. + if ($serviceCandidate.ExePath.ToLower().StartsWith($ServicePath.ToLower())) { + $serviceAlreadyExists = $true + $assemblyinfo = $serviceCandidate.Name + + # SRE-13995 - If the service is already registered, and disabled, we keep it disabled. + if ($serviceCandidate.StartMode -eq 'Disabled') { + $serviceInstallType = $disabledParameterConstant + $StartType = $disabledConstant + Write-Warning "$logLead : SRE-18018 ~ Because the service was already disabled, we will be retaining that status, ignoring input flags, and not starting the service on install ~ `$StartOnInstall = `$false" + $StartOnInstall = $false + } + } else { + Write-Warning "$logLead : This service [$($serviceCandidate.Name)] is installed in [$($serviceCandidate.Path)] and doesn't match what's expected. Removing so we can re-register properly, and avoid bad registrations." + # uninstall here + + # Ensure it isn't running first + Stop-AlkamiService $serviceCandidate.Name + + # making this ICWR due to SRE-16914 which is unexplained. Likely a delay in sc.exe catching up when under heavy system duress. + $icwrSplat = @{ + ScriptBlock = { + param ($sb_serviceCandidate) + Invoke-DeleteLegacyMicroserviceFromServiceCandidate $sb_serviceCandidate + } + Arguments = @($serviceCandidate) + } + Invoke-CommandWithRetry @icwrSplat @icwrTiming + } + } + } + + $exeName = (Split-Path -Path $ServicePath -Leaf) + + $serviceInfo = $null + + # If the service already existed we don't have to install it, but we still do the other things + if (!$serviceAlreadyExists) { + # Do the thing here to install the MS. + Write-Host "$logLead : Installing [$exeName] from $ServicePath" + + $argumentList = @() + + $argumentList += "install" + $argumentList += $serviceInstallType + + Invoke-TopshelfPath $ServicePath $argumentList + Write-Host "$logLead : Successfully finished [$ServicePath] registration of service" + + # This is done to make sure that Windows has a chance to register the service. + Write-Host "$logLead : Sleeping with retry for [$ServicePath] for flushing..." + + $icwrSplat = @{ + ScriptBlock = { + param ($sb_AssemblyInfo) + Write-Host "Getting Service for $sb_sAssemblyInfo..." + return Get-Service $sb_AssemblyInfo + } + + Arguments = @($AssemblyInfo) + } + + $serviceInfo = Invoke-CommandWithRetry @icwrSplat @icwrTiming + } else { + Stop-AlkamiService $AssemblyInfo + + # Powershell 5.1 doesn't support using Set-Service to set the start type to Automatic-Delayed. So we do it with SC.exe + if($StartType -eq $delayedConstant){ + $params = @("config", $serviceCandidate.Name, "start=delayed-auto") + + Invoke-SCExe -Arguments $params + } else { + # Leaving this as is for non-delayed start because it works as is and we don't have unit tests to test a larger change. + Set-Service -ServiceName $Assemblyinfo -StartupType $StartType + } + + $serviceInfo = Get-Service $AssemblyInfo + } + + if ($null -ne $serviceInfo) { + Write-Host "$logLead : Attempt to Set-ChocolateyPackageNewRelicState" + $packageName = (Get-ChocoPackageFromPath $ServicePath) + + if (![string]::IsNullOrWhiteSpace($packageName)) { + Write-Verbose "$logLead : Set-ChocolateyPackageNewRelicState -Name `"$packageName`" -Enabled $SetNewRelicConfiguration" + Set-ChocolateyPackageNewRelicState -Name $packageName -Enabled $SetNewRelicConfiguration + } + } else { + throw "$loglead : Newly registered service could not be found." + } + + # We always call this, whether we just installed it or it already existed + # The reason for this is two-fold. + # 1. In case we are correcting a bad install. + # 2. Resets the gMSA "password already rolled" issue that we use "fixlogins" to resolve + # 2a. This wouldn't resolve "the service got installed 3 months ago" need for "fixlogins", just reinstall + Set-WindowsServiceExecutionAccount -ServiceName $AssemblyInfo -ServiceUser $serviceAccount -IsGMSAAccount + + Set-ServiceRecoveryOneRestart -ServiceName $AssemblyInfo + + # clean up any bad config values + $appConfig = "$ServicePath.config" + + # Not every service has to have a config. This is ok + if (Test-Path $appConfig) { + # get the machine app config value + # Yeah, it's the default parameter, but better to just be direct + $machineConfigKeys = @(Get-AllAppSettingKeys -FilePath (Get-DotNetConfigPath -use64Bit $true)) + $serviceConfigKeys = @(Get-AllAppSettingKeys -FilePath $appConfig) + + # only do the work if we have keys to compare + if (($machineConfigKeys.Count -gt 0) -and ($serviceConfigKeys.Count -gt 0)) { + foreach($key in $serviceConfigKeys) { + if ($machineConfigKeys -contains $key) { + Write-Host "$logLead : Removing [$key] from [$appConfig]" + Remove-AppSetting -Key $key -FilePath $appConfig + } + } + } + } + + if ($StartOnInstall) { + try { + Start-AlkamiService -ServiceName $AssemblyInfo -Timeout $StartTimeout + } catch { + Write-Warning "$logLead : Service could not be started. See logs above." + if ($serviceAlreadyExists) { + throw $_ + } else { + Write-Warning "$logLead : Continuing with install so files will be present post chocolatey install. This is not reporting as an error due to the way chocolatey works during installs, where if it fails it rolls back, but we've already registered the service with Windows. The alternative is to uninstall on failure and let the package be removed." + } + } + } else { + Write-Host "$logLead : Service not started, flag to start was not present or was not set to true" + } + + $stopWatch.Stop() + Write-Host "$logLead : [$assemblyinfo] installed at [$ServicePath] in [$($stopWatch.Elapsed)]" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.ps1 new file mode 100644 index 0000000..c45c163 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.ps1 @@ -0,0 +1,35 @@ +function Invoke-SCExe { +<# +.SYNOPSIS + Used to call sc.exe. This function is basically just a wrapper for unit-testing purposes. + +.PARAMETER Arguments + We have found that an array of strings is fine for sc.exe management. This value just gets splatted as passed in. +#> + [CmdletBinding()] + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $false)] + [string[]]$Arguments = @() + ) + + $logLead = Get-LogLeadName + + # Ensure we have an array + $scArguments = @($Arguments) + + $output = "" + try { + $output = (Invoke-CallOperatorWithPathAndParameters "C:\WINDOWS\System32\sc.exe" $scArguments) + + # Is this always the only way to get an error? + $didFailByExitCode = $LASTEXITCODE -ne 0 + + if ($didFailByExitCode) { + Write-Warning "$logLead : sc.exe failed on run" + throw "sc.exe failed to execute.`r`n$output" + } + } finally { + Write-Verbose "$loglead : sc.exe output: $output" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.tests.ps1 new file mode 100644 index 0000000..851f990 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-SCExe.tests.ps1 @@ -0,0 +1,25 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Invoke-SCExe" { + Context "returns `$LASTEXITCODE -1" { + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { $global:LASTEXITCODE = -1; return "This is output" } + + It "throws as expected" { + { Invoke-SCExe @("ignore") } | Should -Throw + } + } + + Context "returns `$LASTEXITCODE 0" { + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { $global:LASTEXITCODE = 0; return "This is output" } + + It "throws as expected" { + { Invoke-SCExe @("ignore") } | Should -Not -Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.ps1 new file mode 100644 index 0000000..35d641d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.ps1 @@ -0,0 +1,122 @@ +function Invoke-ServiceInstall { + <# + .SYNOPSIS + This function can be called to install a choco service + .DESCRIPTION + This function is used in the pipeline to run migraions and then install the service + .PARAMETER CallingFolder + Full path to the folder containing chocolateyInstall.ps1 for the service to be installed + .PARAMETER SetNewRelicConfiguration + Should the New Relic configuration be applied to this service? + .EXAMPLE + Invoke-ServiceInstall -CallingFolder C:\ProgramData\chocolatey\lib\alkami.ms.package.name\tools -SetNewRelicConfiguration $true + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$CallingFolder, + [Parameter(Mandatory = $false)] + [switch]$SetNewRelicConfiguration = $false + ) + + $logLead = Get-LogLeadName + + # Handle inputs where callers followed old helptext and + # passed the full filename path to chocoInstall.ps1 + # We need the path to the folder that *contains* chocoInstall.ps1 + if (Test-Path -Path $CallingFolder -PathType Container) { + $folderWithChocoInstall = $CallingFolder + } else { + $folderWithChocoInstall = Split-Path -Path $CallingFolder -Parent + } + + $packageFolder = Split-Path -Path $folderWithChocoInstall + $manifestData = Get-PackageManifest -Path $packageFolder + + $serviceManifest = $manifestData.serviceManifest + $databaseAccessRequired = Test-ServiceManifestRequiresDbAccess -ServiceManifest $serviceManifest + $hasMigrations = Test-ServiceManifestHasMigrations -ServiceManifest $serviceManifest + $isDeveloperMachine = (Test-IsDeveloperMachine) -and (!(Test-IsAws)) + + # Massage the runtime type + $runtime = $serviceManifest.runtime + if ($runtime -eq "legacy") { + $runtime = "framework" + } + if ($runtime -eq "core") { + $runtime = "dotnetcore" + } + + $parameters = @{ + ServicePath = $packageFolder + IsDatabaseAccessRequired = $databaseAccessRequired + StartOnInstall = $isDeveloperMachine + AssemblyInfo = $serviceManifest.entryPoint + SetNewRelicConfiguration = $SetNewRelicConfiguration + } + + # Only run if pkg has migrations and on dev machine + if ($hasMigrations -eq $true) { + if ($isDeveloperMachine -eq $true) { + $missingAssemblies = @() + $assemblyNames = $serviceManifest.migrations.assembly.assembly + foreach ($assemblyName in $assemblyNames) { + # Our AlkamiManifest standard for Asssembly names is "flexible" + # We handle assembly names that have extensions or not + # simplitic regex : + # `^` means "beginning of line/string" **in this context** + # ".+" means "one or more of any character" + # "\." means "one dot" + # "dll" means "dll" + # `$` means "end of line/string" **in this context** + # so... + # something that ends in ".dll" + if ($assemblyName -match "^.+\.dll$") { + Write-Verbose "$loglead : Assembly name already has dll extension" + $assemblyFilename = $assemblyName + } else { + Write-Verbose "$loglead : Adding dll extension to assembly name" + $assemblyFilename = "$($assemblyName).dll" + } + $assemblyFile = Get-ChildItem -Path $packageFolder -Recurse -Filter $assemblyFilename -File + if (-not $assemblyFile) { + $missingAssemblies += $assemblyName + + } + } + + if (-NOT (Test-IsCollectionNullOrEmpty -Collection $missingAssemblies)) { + $missingAssembliesString = $missingAssemblies -join "," + throw "MISSING MIGRATION ASSEMBLIES - $missingAssembliesString" + } + + + $nuspecFSO = Get-ChildItem -Path $packageFolder -Filter "*.nuspec" | Select-Object -First 1 + $nuspecXml = [xml](Get-Content -Path $nuspecFSO.FullName) + $packageId = $nuspecXml.package.metadata.id + $packageVersion = $nuspecXml.package.metadata.version + + Invoke-AlkamiMigrationRunner -Runtime $runtime -PackageId $packageId -PackageVersion $packageVersion + } + } + + switch ($runtime) { + "dotnetcore" { + Write-Host "$logLead : Installing a dotnetcore service" + Install-AlkamiService @parameters + <# installing migrations goes here #> + } + "framework" { + Write-Host "$logLead : Installing a framework microservice" + Install-LegacyMicroservice @parameters + <# installing migrations goes here #> + } + <# "nodejs" { + throw "this is where node would go" + } #> + default { + Write-Error "$logLead : Nothing to install for [$packageFolder] or is unsupported runtime for [$runtime]" + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.tests.ps1 new file mode 100644 index 0000000..edcbf32 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-ServiceInstall.tests.ps1 @@ -0,0 +1,138 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Invoke-ServiceInstall" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Split-Path -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-PackageManifest -MockWith { return @{ + ServiceManifest = @{ + runtime = "core" + Migrations = @{ + Assembly = @{ + role = "MyAssemblyRole" + } + } + } + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestHasMigrations -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Split-Path -MockWith { return "Path" } + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Test-IsAws -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem -MockWith { return @{ FullName = "SomeName" } } + Mock -ModuleName $moduleForMock -CommandName Get-Content -MockWith { + $nuspec = @" + + + SomePackageId + 1.0.0 + + +"@ + + return ($nuspec) + } + + Mock -ModuleName $moduleForMock -CommandName Invoke-AlkamiMigrationRunner -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Install-AlkamiService -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Install-LegacyMicroservice -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { $true } -ParameterFilter { + $CallingFolder -eq "SomeFolder" -and $PathType -eq "Container" } + + + Context "When Migrations Should Be Run On A Developer Machine" { + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestRequiresDbAccess -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestHasMigrations -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { return $true } + + It "Runs Migrations" { + Invoke-ServiceInstall -CallingFolder "SomeFolder" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Invoke-AlkamiMigrationRunner + } + } + + Context "When Installing A DotNetCore Service" { + Mock -ModuleName $moduleForMock -CommandName Get-PackageManifest -MockWith { return @{ + ServiceManifest = @{ + runtime = "core" + Migrations = @{ + Assembly = @{ + role = "MyAssemblyRole" + } + } + } + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestRequiresDbAccess -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestHasMigrations -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { return $false } + + It "Calls Install-AlkamiService" { + Invoke-ServiceInstall -CallingFolder "SomeFolder" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-AlkamiService + } + } + + Context "When Installing A Legacy Framework Service" { + + Mock -ModuleName $moduleForMock -CommandName Get-PackageManifest -MockWith { return @{ + ServiceManifest = @{ + runtime = "legacy" + Migrations = @{ + Assembly = @{ + role = "MyAssemblyRole" + } + } + } + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestRequiresDbAccess -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestHasMigrations -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { return $false } + + It "Calls Install-LegacyMicroservice" { + Invoke-ServiceInstall -CallingFolder "SomeFolder" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Install-LegacyMicroservice + } + } + + + Context "When Framework Cannot Be Determined" { + Mock -ModuleName $moduleForMock -CommandName Get-PackageManifest -MockWith { return @{ + ServiceManifest = @{ + runtime = "blargh" + Migrations = @{ + Assembly = @{ + role = "MyAssemblyRole" + } + } + } + } + } + + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestRequiresDbAccess -MockWith { $true } + Mock -ModuleName $moduleForMock -CommandName Test-ServiceManifestHasMigrations -MockWith { return $true } + Mock -ModuleName $moduleForMock -CommandName Test-IsDeveloperMachine -MockWith { return $false } + + It "Writes An Error" { + Invoke-ServiceInstall -CallingFolder "SomeFolder" + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Write-Error -ParameterFilter { $Message -match "Nothing to install for" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.ps1 new file mode 100644 index 0000000..223e1ad --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.ps1 @@ -0,0 +1,55 @@ +function Invoke-TopshelfPath { +<# +.SYNOPSIS + Used to call a topshelf path and look for errors. + This function is basically just a wrapper for unit-testing purposes. + +.PARAMETER Path + The file path being invoked + +.PARAMETER Arguments + This value just gets splatted as passed in. +#> + [CmdletBinding()] + [OutputType([System.Object])] + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [Parameter(Mandatory=$true)] + [string[]]$Arguments + ) + + $logLead = Get-LogLeadName + + try { + $output = @(Invoke-CallOperatorWithPathAndParameters $Path @($Arguments)) + + # This doesn't usually return an exit code for Topshelf + $didFailByExitCode = ($LASTEXITCODE -ne 0) + $errorMessage = "Service failed by return of exit code other than 0" + + $didFailByErrorText = $false + # Checking the first line of output for the text Error. + if ($output[0] -match "Error") { + $errorMessage = $output[1] + $didFailByErrorText = $true + } + + # look for other exceptions. This won't catch everything, but should get a lot. + foreach ($line in $output) { + if ($line -match "Win32Exception") { + $errorMessage = $line.Split(':', 2) + $didFailByErrorText = $true + } + } + + Write-Verbose ($output -join '`r`n') + if ($didFailByExitCode -or $didFailByErrorText) { + Write-Warning "$logLead : [$Path] failed on run. Output printed to verbose stream above." + throw "[$Path] failed to execute.`r`n$errorMessage" + } + } finally { + # Keeping a `finally` because you can't have a try without either `catch` or `finally` + Write-Verbose "$logLead : done" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.tests.ps1 new file mode 100644 index 0000000..838c5a8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Invoke-TopshelfPath.tests.ps1 @@ -0,0 +1,67 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Invoke-TopshelfPath" { + Context "throws on an array with error text" { + $output = @( + "Topshelf.HostFactory Error: 0 : An exception occurred creating the host, Topshelf.HostConfigurationException: The service was not properly configured: ", + "[Failure] Command Line An unknown command-line option was found: ARGUMENT: -?", + "[Success] Name Alkami.Services.Subscriptions.Host", + "[Success] ServiceName Alkami.Services.Subscriptions.Host", + " at Topshelf.Configurators.ValidateConfigurationResult.CompileResults(IEnumerable`1 results)", + " at Topshelf.HostFactory.New(Action`1 configureCallback)", + "Topshelf.HostFactory Error: 0 : The service terminated abnormally, Topshelf.HostConfigurationException: The service was not properly configured: ", + "[Failure] Command Line An unknown command-line option was found: ARGUMENT: -?", + "[Success] Name Alkami.Services.Subscriptions.Host", + "[Success] ServiceName Alkami.Services.Subscriptions.Host", + " at Topshelf.Configurators.ValidateConfigurationResult.CompileResults(IEnumerable`1 results)", + " at Topshelf.HostFactory.New(Action`1 configureCallback)", + " at Topshelf.HostFactory.Run(Action`1 configureCallback)" + ) + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return $output } + + It "throws as expected" { + { Invoke-TopshelfPath "ignored" @("ignored") } | Should -Throw + } + } + + Context "throws on an array with Win32Exception text" { + $output = @( + "Running a transacted installation.", + "Beginning the Install phase of the installation.", + "Installing service Alkami.MS.RDC.Ensenta.Service.Host...", + "Creating EventLog source Alkami.MS.RDC.Ensenta.Service.Host in log Application...", + "An exception occurred during the Install phase.", + "System.ComponentModel.Win32Exception: The name is already in use as either a service name or a service display name", + "The Rollback phase of the installation is beginning.", + "Restoring event log to previous state for source Alkami.MS.RDC.Ensenta.Service.Host.", + "The Rollback phase completed successfully.", + "The transacted install has completed." + ) + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { return $output } + + It "throws as expected" { + { Invoke-TopshelfPath "ignored" @("ignored") } | Should -Throw + } + } + + Context "doesn't throw if no exit code is present" { + $output = @( + "Configuration Result:", + "[Success] Name Alkami.Services.Subscriptions.Host", + "[Success] ServiceName Alkami.Services.Subscriptions.Host", + "Topshelf v3.1.122.0, .NET Framework v4.0.30319.42000", + "The Alkami.Services.Subscriptions.Host service is already installed." + ) + Mock -ModuleName $moduleForMock -CommandName Invoke-CallOperatorWithPathAndParameters -MockWith { $global:LASTEXITCODE = 0; return $output } + + It "does not throw (as expected)" { + { Invoke-TopshelfPath "ignored" @("ignored") } | Should -Not -Throw + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.ps1 new file mode 100644 index 0000000..a44fe8a --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.ps1 @@ -0,0 +1,56 @@ +function New-AppTierWindowsServices { +<# +.SYNOPSIS + Used to configure Windows services for Install\-ORBAppServer +#> + [CmdletBinding()] + Param() + + $logLead = (Get-LogLeadName) + + $servicesToInstall = (Get-AppTierServices) + + foreach ($service in $servicesToInstall) { + # The service object has properties: + # @{ FolderName; AssemblyInfo; Name; User; Password; IsGMSAAccount; Binary; } + # They are used as such: + # Install-AlkamiService uses AssemblyInfo, Name, User + # Set-WindowsServiceExecutionAccount uses Name, User, Password, IsGMSAAccount + # Binary is unused, it is legacy. It's useful, just not for this, due to the way Install-AlkamiService works + + $serviceName = $service.Name + $serviceFolder = (Join-Path (Get-OrbPath) $service.FolderName) # The highly legacy service applications run from ORB proper + $assemblyInfo = $service.AssemblyInfo + $user = $service.User + $password = $service.Password + $IsGMSA = $service.IsGMSAAccount + + try { + Write-Host "$logLead : Beginning install of legacy service [$serviceName]." + + $splat = @{ + Path = $serviceFolder + AssemblyInfo = $assemblyInfo + # ex: "Alkami Nag Service" - Should get set on the service at create or update time + DisplayName = $serviceName + # SRE-17892 - all two of the default Alkami Windows Services require database access + IsDatabaseAccessRequired = $true + StartOnInstall = $false + } + + # Install-AlkamiService is self-healing + Install-AlkamiService @splat + + Write-Host "$logLead : Service installed, updating service execution account for [$serviceName]" + + # Set-WindowsServiceExecutionAccount is self-healing + Set-WindowsServiceExecutionAccount -ServiceName $serviceName -ServiceUser $user -ServicePassword $password -IsGMSAAccount:$IsGMSA + + Write-Host "$logLead : Service [$serviceName] appears to finish installing correctly." + } catch { + Write-Warning "$logLead : Failed to finish creating account. See previous log statements for last good location. Continuing to next service." + } + } +} + +Set-Alias -name Create-AppTierWindowsServices -value New-AppTierWindowsServices \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.tests.ps1 new file mode 100644 index 0000000..2c1215d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/New-AppTierWindowsServices.tests.ps1 @@ -0,0 +1,69 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "New-AppTierWindowsServices" { + Context "Ensure that it does the simple happy path for one service defined" { + Mock -ModuleName $moduleForMock -CommandName Get-AppTierServices -MockWith { @( + @{ + FolderName = 'Fake Path' + AssemblyInfo = 'Fake.Service.exe' + Name = "Fake Service" + User = "REPLACEME" + Password = "REPLACEME" + IsGMSAAccount = $true + Binary = $basePath + "\Nag\Alkami.App.Nag.Host.Service.exe" + } + ) } + + Mock -ModuleName $moduleForMock -CommandName Install-AlkamiService -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Set-WindowsServiceExecutionAccount -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-OrbPath -MockWith { return "fakepath" } + + + It "Does not throw" { + { New-AppTierWindowsServices } | Should -Not -Throw + } + + It "Calls Install-AlkamiService once for a single service being called" { + New-AppTierWindowsServices + Assert-MockCalled -CommandName Install-AlkamiService -Times 1 -Scope It -ModuleName $moduleForMock + } + + It "Calls Set-WindowsServiceExecutionAccount once for a single service being called" { + New-AppTierWindowsServices + Assert-MockCalled -CommandName Set-WindowsServiceExecutionAccount -Times 1 -Scope It -ModuleName $moduleForMock + } + } + + Context "Ensure that it does not call inner functions for empty array" { + Mock -ModuleName $moduleForMock -CommandName Get-AppTierServices -MockWith { @() } + + Mock -ModuleName $moduleForMock -CommandName Install-AlkamiService -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Set-WindowsServiceExecutionAccount -MockWith { } + + Mock -ModuleName $moduleForMock -CommandName Get-OrbPath -MockWith { return "fakepath" } + + + It "Does not throw" { + { New-AppTierWindowsServices } | Should -Not -Throw + } + + It "Does not call Install-AlkamiService if no service being called to install" { + New-AppTierWindowsServices + Assert-MockCalled -CommandName Install-AlkamiService -Times 0 -Scope It -ModuleName $moduleForMock + } + + It "Does not call Set-WindowsServiceExecutionAccount if no service being called to install" { + New-AppTierWindowsServices + Assert-MockCalled -CommandName Set-WindowsServiceExecutionAccount -Times 0 -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Ping-AlkamiServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/Ping-AlkamiServices.ps1 new file mode 100644 index 0000000..19dcfe0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Ping-AlkamiServices.ps1 @@ -0,0 +1,174 @@ +function Ping-AlkamiServices { + <# + .SYNOPSIS + Warms up app tier web services + + .PARAMETER SkipOutput + Suppresses function output + + .PARAMETER SkipCheck + When passed, doesn't check to determine if the calling machine is a web server + #> + [CmdletBinding()] + [OutputType([System.Object])] + Param( + # This can be used to consume the output as an object in a downstream function + # If not included the output is formatted for review by a human + [Parameter(Mandatory = $false)] + [Alias("NoOutput")] + [switch]$skipOutput, + + [Parameter(Mandatory = $false)] + [Alias("Force")] + [switch]$skipCheck + ) + + $logLead = Get-LogLeadName + + if ((Test-IsWebServer) -and !($skipCheck)) { + # Exit Early + Write-Host "$logLead : This is not a valid function for a web server" + return + } + + $maxJobs = 3 + $jobs = @() + $testPattern = "mexHttpBinding" + + # Ensure we can actually hit the services before we try to warm them up + # If we can not hit them, this will throw, as currently written, so we should not proceed + Test-KnownWCFServicesResolvable + + # Needed for local testing + #Import-Module WebAdministration + #[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Administration") + $mgr = New-Object Microsoft.Web.Administration.ServerManager + + $functionStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + $applicationsToWarmUp = @() + + # $appTierApplications is a global variable. + foreach ($service in $appTierApplications) { + $targetSite = $mgr.Sites | Where-Object { ($_ | Select-Object -ExpandProperty Applications | Select-Object -ExpandProperty Path) -match $service.Name } | Select-Object -First 1 + + if ($null -eq $targetSite) { + Write-Warning ("{1} : Could not find any website which contains application {0}" -f $service.Name, $logLead) + continue + } + + $httpBinding = $targetSite.Bindings | Where-Object { $_.Protocol -ne "net.tcp" } | Select-Object -First 1 + + if ($null -eq $httpBinding) { + Write-Warning ("{2} : Could not find any http/https binding for site {0} hosting application {1}" -f $targetSite.Name, $service.Name, $logLead) + continue + } + + $hostName = "localhost" + if ($httpBinding.Protocol -eq "https") { + $hostName = $httpBinding.Host + } + + $urlString = "{0}://{1}/{2}/{3}" -f $httpBinding.Protocol, $hostName, $service.WebAppName, $service.Endpoint + + Write-Host ("{2} : Preparing to warm up application {0} with URL {1}" -f $service.WebAppName, $urlString, $logLead) + $applicationsToWarmUp += @{ Url = $urlString; Endpoint = $service.Endpoint; WebAppName = $service.WebAppName } + } + + $scriptBlock = { + + param ($service, $testPattern) + + $logLead = "[Ping-AlkamiServices]" + try { + add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ + + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + $response = Invoke-WebRequest -Uri $service.Url -UseBasicParsing -TimeoutSec 300 + $stopWatch.Stop() + + if ($response.StatusCode -ne 200 -or $response.Content -notmatch $testPattern) { + return New-Object -TypeName PSObject -Property @{ + URL = $service.Url.ToLowerInvariant() + ServiceEndpoint = $service.Endpoint.ToLowerInvariant() + Success = $false + StatusCode = $response.StatusCode + Elapsed = $stopWatch.Elapsed.ToString() + WebAppName = $service.WebAppName + } + } else { + return New-Object -TypeName PSObject -Property @{ + URL = $service.Url.ToLowerInvariant() + ServiceEndpoint = $service.Endpoint.ToLowerInvariant() + Success = $true + StatusCode = $response.StatusCode + Elapsed = $stopWatch.Elapsed.ToString() + WebAppName = $service.WebAppName + } + } + } catch { + Write-Warning "$logLead : Exception caught! $(Resolve-Error $_)" + return New-Object -TypeName PSObject -Property @{ + URL = $service.Url.ToLowerInvariant() + ServiceEndpoint = $service.Endpoint.ToLowerInvariant() + Success = $false + StatusCode = $response.StatusCode + Elapsed = $stopWatch.Elapsed.ToString() + WebAppName = $service.WebAppName + } + } + } + + $serviceResults = @() + + foreach ($service in ($applicationsToWarmUp | Where-Object { $null -ne $_.Url })) { + $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList $service, $testPattern + $running = @($jobs | Where-Object { $_.State -eq 'Running' }) + + while ($running.Count -ge $maxJobs -and $running.Count -ne 0) { + (Wait-Job -Job $jobs -Any) | Out-Null + $running = @($jobs | Where-Object { $_.State -eq 'Running' }) + } + } + + if ($jobs) { + Wait-Job -Job $jobs > $null + } + + $failed = @($jobs | Where-Object { $_.State -eq 'Failed' }) + if ($failed.Count -gt 0) { + $failed | ForEach-Object { $_.ChildJobs[0].JobStateInfo.Reason.Message } + } + + $jobs | ForEach-Object { + $serviceResults += $_ | Receive-Job | Select-Object URL, ServiceEndpoint, Success, StatusCode, Elapsed, WebAppName + } + + $functionStopWatch.Stop() + + if ($skipOutput) { + return $serviceResults + } else { + if ($null -ne ($serviceResults | Where-Object {$_.Success -eq $false})) { + Write-Warning "$logLead : One or more URLs failed the test case:`n" + } + + $result = ($serviceResults | Sort-Object -Property Success | Format-Table -Property @{Label="URL";Width=70;e={$_.URL};Alignment="Left"}, @{Label="Success";Width=15;e={$_.Success};Alignment="Right"},@{Label="Elapsed";Width=25;e={$_.Elapsed};Alignment="Right"} | Out-String) + Write-Host $result + Write-Host ("{1} : Total Execution Time: {0}" -f $functionStopWatch.Elapsed.ToString(), $logLead) + + return $serviceResults + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Resources/TriggerQuery.sql b/Modules/Alkami.PowerShell.Services/Public/Resources/TriggerQuery.sql new file mode 100644 index 0000000..1463fd4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Resources/TriggerQuery.sql @@ -0,0 +1,5 @@ +SELECT ISNULL(t.NEXT_FIRE_TIME, '0') [FireTime], ISNULL(ft.SCHED_TIME, '0') [ScheduledTime], ISNULL(t.JOB_NAME, ft.JOB_NAME) [JobName] +FROM [dbo].[NAG_TRIGGERS] t (nolock) + LEFT JOIN [dbo].[NAG_FIRED_TRIGGERS] ft on ft.TRIGGER_NAME = t.TRIGGER_NAME +WHERE (t.TRIGGER_NAME like '%ScheduledTransfers%' or t.TRIGGER_NAME like '%AccountBatchFileImportJob%' or t.TRIGGER_NAME like '%CenlarBatchFileImport%') + AND (ft.SCHED_TIME IS NOT NULL OR (t.NEXT_FIRE_TIME > @now AND t.NEXT_FIRE_TIME < @threshold)) \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.Tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.Tests.ps1 new file mode 100644 index 0000000..5099b4a --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.Tests.ps1 @@ -0,0 +1,198 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Restart-Nag" { + Mock -ModuleName $moduleForMock -CommandName Write-Warning {} + Mock -ModuleName $moduleForMock -CommandName Test-AreCriticalNagJobsRunning {} + Mock -ModuleName $moduleForMock -CommandName Stop-AlkamiService {} + Mock -ModuleName $moduleForMock -CommandName Write-Verbose {} + Mock -ModuleName $moduleForMock -CommandName Write-Output {} + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName {} + Mock -ModuleName $moduleForMock -CommandName Get-Service {} + + Context "Can Recycle Checks" { + It "Warns when Nag is not present" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return $null } + + Restart-Nag + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -eq " : The Alkami Nag Service could not be located" + } + } + + It "Does not throw when Nag is not present" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return $null } + + { Restart-Nag } | Should -Not -Throw + } + + It "Warns if Nag is disabled." { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Disabled" + } } + Restart-Nag + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -eq " : The Alkami Nag Service is disabled and cannot be restarted" + } + } + + It "Does not throw if Nag is disabled." { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Disabled" + } } + + { Restart-Nag } | Should -Not -Throw + } + + It "Warns if user is Anonymous" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem { + $userInfo = @{} + $userInfo.Name = "USERNAME" + $userInfo.Value = "ANONYMOUS" + return $userInfo + } -ParameterFilter { $Path -and $Path -eq "env:USERNAME" } + + Restart-Nag + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -eq " : Anonymous users cannot query the database. This function cannot be executed under the current user context" + } + } + + It "Does not throw if user is Anonymous" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem { + $userInfo = @{} + $userInfo.Name = "USERNAME" + $userInfo.Value = "ANONYMOUS" + return $userInfo + } -ParameterFilter { $Path -and $Path -eq "env:USERNAME" } + + { Restart-Nag } | Should -Not -Throw + } + + It "Warns if Host is Chocolatey" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem { return "IAmAValidUser" } -ParameterFilter { $Path -and $Path -eq "env:USERNAME" } + Mock -ModuleName $moduleForMock -CommandName Get-Host { return @{Name = "Chocolatey_PSHost" } } + + Restart-Nag + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -eq " : You can't run this from Chocolatey without the -Force flag because we don't know if this is a local or remote session" + } + } + + It "Does not throw if Host is Chocolatey" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Get-ChildItem { return "IAmAValidUser" } -ParameterFilter { $Path -and $Path -eq "env:USERNAME" } + Mock -ModuleName $moduleForMock -CommandName Get-Host { return @{Name = "Chocolatey_PSHost" } } + + { Restart-Nag } | Should -Not -Throw + } + + It "Recycles Nag if Force Flag is present." { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Running" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Stop-AlkamiService -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Start-Service -MockWith { return $null } + + Restart-Nag -Force + + Assert-MockCalled -ModuleName $moduleForMock Stop-AlkamiService + Assert-MockCalled -ModuleName $moduleForMock Start-Service + } + } + + Context "Recycle Tests" { + Mock -ModuleName $moduleForMock -CommandName Stop-AlkamiService -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Start-Service -MockWith { return $null } + + It "Recycles Nag When Service is Stopped" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Stopped" + StartType = "Manual" + } } + + Restart-Nag + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService + Assert-MockCalled -ModuleName $moduleForMock -CommandName Start-Service + } + + It "Tests for running Nag Jobs if necessary" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Running" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Test-AreCriticalNagJobsRunning -MockWith { return $true } + + Restart-Nag + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Test-AreCriticalNagJobsRunning + } + + It "Warns and does not recycle Nag if jobs are running" { + Mock -ModuleName $moduleForMock -CommandName Get-Service { return @{ + DisplayName = "Alkami Nag Service" + Name = "Alkami Nag Service" + Status = "Running" + StartType = "Manual" + } } + + Mock -ModuleName $moduleForMock -CommandName Test-AreCriticalNagJobsRunning -MockWith { return $false } -Verifiable + + Restart-Nag + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -eq " : Cannot recycle Alkami Nag Service due to running or scheduled jobs" + } + + Assert-MockCalled -ModuleName $moduleForMock -CommandName Stop-AlkamiService -Exactly 0 -Scope It + Assert-MockCalled -ModuleName $moduleForMock -CommandName Start-Service -Exactly 0 -Scope It + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.ps1 b/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.ps1 new file mode 100644 index 0000000..d3f0c68 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Restart-Nag.ps1 @@ -0,0 +1,71 @@ +function Restart-Nag { +<# +.SYNOPSIS + Checks Running Jobs and Restarts Nag When Safe to Do So + +.PARAMETER threshold + +Alias: Minutes +The number of minutes in the future to check for scheduled jobs +.PARAMETER forceRecycle + +Alias: Force +Force recycle even if jobs are running or scheduled within the cutoff time +#> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false)] + [Alias("Minutes")] + [int]$threshold = 5, + + [Parameter(Mandatory = $false)] + [Alias("Force")] + [switch]$forceRecycle + ) + + $canRecycle = $false + $hostName = Get-Host + $logLead = (Get-LogLeadName); + $skipJobCheck = $forceRecycle.IsPresent + $userName = Get-ChildItem env:USERNAME + + $nagService = Get-Service | Where-Object {$_.Name -match "Alkami.Nag"} + + # If the Nag service isn't found or is disabled, warn and return + if ($null -eq $nagService) { + Write-Warning ("$logLead : The Alkami Nag Service could not be located") + return + } elseif ($nagService.StartType -eq "Disabled") { + Write-Warning ("$logLead : The Alkami Nag Service is disabled and cannot be restarted") + return + } elseif ($userName.Value -match "ANONYMOUS") { + Write-Warning ("$logLead : Anonymous users cannot query the database. This function cannot be executed under the current user context") + return + } elseif ($hostName.Name -match "Chocolatey_PSHost" -and !$skipJobCheck) { + Write-Warning ("$logLead : You can't run this from Chocolatey without the -Force flag because we don't know if this is a local or remote session") + return + } + + # If the Nag Service is already stopped, we don't need to check if we can shut it down + if ($nagService.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Stopped) { + $skipJobCheck = $true + } + + if (!$skipJobCheck) { + $canRecycle = Test-AreCriticalNagJobsRunning $threshold + } else { + Write-Verbose ("$logLead : Force Recycle Flag Passed or Service Was Already Stopped") + $canRecycle = $true + } + + if ($canRecycle) { + # Recycle the service + Write-Output "$logLead : Recycling Nag Service" + Stop-AlkamiService -ServiceName $nagService.Name -Seconds 45 + Start-Service -name $nagService.name + } else { + # Warn that we cannot recycle the service + Write-Warning ("$logLead : Cannot recycle {0} due to running or scheduled jobs" -f $nagService.Name) + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.Tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.Tests.ps1 new file mode 100644 index 0000000..6f77c38 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.Tests.ps1 @@ -0,0 +1,90 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Set-ChocolateyPackageNewRelicState + +Describe "Set-ChocolateyPackageNewRelicState" { + + $global:nrXmlTrue = @' + + + + + + +'@ + + $global:nrXmlFalse = @' + + + + + + +'@ + + $global:nrXmlMissing = @' + + + + + + +'@ + + $global:nrXmlNoAppSettings = @' + + + +'@ + + # Mock the pieces that make sure that the app.config is a real file. + Mock -ModuleName $moduleForMock Get-ChildItem { return "totally real file path.jpg" } + Mock -ModuleName $moduleForMock Test-Path { return $true } + Mock -ModuleName $moduleForMock Save-XMLFile { } + Mock -ModuleName $moduleForMock Get-UncPath { } + Mock -ModuleName $moduleForMock Set-DotNetCoreProfiling { } + + Context "Sets NewRelic.AgentEnabled" { + + # Mock Read-XmlFile for the Test + Mock -ModuleName $moduleForMock Read-XmlFile { return [xml]($nrXmlTrue.Clone()) } + Mock -ModuleName $moduleForMock Test-Path { return $true } + + Mock -ModuleName $moduleForMock Set-AppSetting { } + + It "Sets value to false" { + Set-ChocolateyPackageNewRelicState -Name "fake.package" -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Set-AppSetting -Times 1 -Exactly -Scope It -ParameterFilter { + $Key -eq "NewRelic.AgentEnabled" -and $Value -eq "false" + } + } + + It "Sets value to true" { + Set-ChocolateyPackageNewRelicState -Name "fake.package" -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock Set-AppSetting -Times 1 -Exactly -Scope It -ParameterFilter { + $Key -eq "NewRelic.AgentEnabled" -and $Value -eq "true" + } + } + } + + Context "Package Path Doesn't Exist" { + # Mock Test-Path, which is only used to determine if the microservice path exists. + Mock -ModuleName $moduleForMock Test-Path { return $false } + Mock -ModuleName $moduleForMock Write-Warning { } + + It "Warns if App.Config Path Doesn't Exist" { + Set-ChocolateyPackageNewRelicState -Name "fake.package" -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It -ParameterFilter { + $message -like "*Could not locate app.config path*" + } + } + } +} + +#endregion Set-ChocolateyPackageNewRelicState diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.ps1 new file mode 100644 index 0000000..8190224 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ChocolateyPackageNewRelicState.ps1 @@ -0,0 +1,74 @@ +function Set-ChocolateyPackageNewRelicState { + <# + .SYNOPSIS + Enables or disables new relic for a specific microservice chocolatey package. The microservice still requires a restart before it will take effect. + .PARAMETER Name + The name of the package to set the new-relic state for. + .PARAMETER Directory + The directory to search for app.configs and change the new relic state. + .PARAMETER Enabled + True to enable New Relic, false to disable it. + #> + [CmdletBinding(DefaultParameterSetName = 'ByPackageName')] + Param( + [Parameter(ParameterSetName = 'ByPackageName', Mandatory = $true)] + [string]$Name, + + [Parameter(ParameterSetName = 'ByDirectory', Mandatory = $true)] + [string]$Directory, + + [Parameter(ParameterSetName = 'ByDirectory', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByPackageName', Mandatory = $true)] + [bool]$Enabled + ) + + $loglead = (Get-LogLeadName) + + # $Name$Directory will write both values, but it can only be one or the other. Cheap hack putting them both in the same brackets + Write-Host "$logLead : Setting NR state for [$Name$Directory]" + + $directoryToSearch = $null + if($PSCmdlet.ParameterSetName -eq "ByPackageName") { + # Construct the path to the microservice in the chocolatey install path. + $chocoPath = (Get-ChocolateyInstallPath) + $chocoPath = Join-Path $chocoPath "lib" + $chocoPath = Join-Path $chocoPath $name + + $directoryToSearch = $chocoPath + } elseif($PSCmdlet.ParameterSetName -eq "ByDirectory") { + $directoryToSearch = $Directory + } + + # $Name$Directory will write both values, but it can only be one or the other. Cheap hack putting them both in the same brackets + Write-Host "$logLead : Found [$directoryToSearch] for [$Name$Directory]" + + if(!(Test-Path $directoryToSearch)) { + Write-Warning "$loglead : Could not locate app.config path '$directoryToSearch'" + return + } + + # Find the app configs. Start with "legacy" .exe.config for framework apps + $appConfigs = (Get-ChildItem -Path $directoryToSearch -Include "*.exe.config" -Recurse) + + # If no .exe.config, look for an appsettings.json + if(Test-IsCollectionNullOrEmpty $appConfigs) { + $appConfigs = (Get-ChildItem -Path $directoryToSearch -Include "appsettings.json" -Recurse) + } + + if(Test-IsCollectionNullOrEmpty $appConfigs) { + Write-Warning "$loglead : Could not locate app.config for $Name" + return + } + + # Determine the value of the app setting. + $desiredValue = if($Enabled) { "true" } else { "false" } + + # Update all the app.configs. + foreach($configPath in $appConfigs) { + Set-AppSetting -FilePath $configPath -Key "NewRelic.AgentEnabled" -Value $desiredValue -UpdateOnly + } + + Set-DotNetCoreProfiling -Path $directoryToSearch -Enabled $enabled + + Write-Host "$logLead : Finished setting NR state for [$directoryToSearch]" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.ps1 new file mode 100644 index 0000000..1473339 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.ps1 @@ -0,0 +1,102 @@ +function Set-DotNetCoreProfiling { + <# + .SYNOPSIS + Enables or disables profiling for a specific dotnet core microservice chocolatey package. The microservice still requires a restart before it will take effect. + .PARAMETER Name + The Name of the package to set the new-relic state for. + .PARAMETER Path + The Path of the package to set the new-relic state for. + .PARAMETER Enabled + True to enable New Relic, false to disable it. + #> + [CmdletBinding()] + Param( + [Parameter(ParameterSetName = 'ByPackageName', Mandatory = $true)] + [string]$Name, + + [Parameter(ParameterSetName = 'ByPath', Mandatory = $true)] + [string]$Path, + + [Parameter(ParameterSetName = 'ByPath', Mandatory = $true)] + [Parameter(ParameterSetName = 'ByPackageName', Mandatory = $true)] + [bool]$Enabled + ) + + $loglead = Get-LogLeadName + + # $Name$Path will write both values, but it can only be one or the other. Cheap hack putting them both in the same brackets + Write-Host "$logLead : Setting CORECLR_ENABLE_PROFILING state for [$Name$Path]" + + if ($PSCmdlet.ParameterSetName -eq "ByPackageName") { + # Construct the path to the microservice in the chocolatey install path. + $chocoPath = (Get-ChocolateyInstallPath) + $chocoPath = Join-Path $chocoPath "lib" + $chocoPath = Join-Path $chocoPath $Name + $Path = $chocoPath + } else { + # Name of service is choco lib folder Name + $Name = Split-Path -Path $Path -Leaf + } + + $clrString = "CORECLR_ENABLE_PROFILING" + $regKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$Name" + $nullSeperator = $([char]0) + + Write-Host "$loglead : Determining if [$Name] is Dot Net Core service" + try { + $packageManifest = Get-PackageManifest -Path $Path -PackageName $Name + $IsServiceManifestCore = Test-IsServiceManifestCore -ServiceManifest $packageManifest + } + catch { + Write-Warning "$logLead : Service [$Name] Unable to determine service runtime" + Write-warning $_ + } + if ($IsServiceManifestCore -eq $true ) { + Write-Host "$logLead : Service [$Name] Setting [$clrString] [$($Enabled.toString())]" + if ($enabled -eq $true) { + $keyValue = "1" + } else { + $keyValue = "0" + } + try { + $shouldSetKey = $true + $rebuildString = "" + $regKey = Get-ItemProperty -Path $regKeyPath + $environmentKey = $regKey.Environment + + [Array]$environmentValues = $environmentKey + foreach ($environmentValue in $environmentValues) { + if ($environmentValue -eq "$clrString=0") { + Write-Host "$loglead : [$environmentValue] CLR currently set to disabled" + if ($Enabled -eq $false) { + $shouldSetKey = $false + } + } elseif ($environmentValue -eq "$clrString=1") { + Write-Host "$loglead : [$environmentValue] CLR currently set to enabled" + if ($Enabled -eq $true) { + $shouldSetKey = $false + } + } else { + if ($environmentValue -ne $nullSeperator) { + Write-Host "$loglead : [$environmentValue] is not clr string" + $rebuildString = $rebuildString + $environmentValue + $nullSeperator + } + } + } + } catch { + Write-Host "$loglead : Failed to find Core clr string" + } + if ($shouldSetKey -eq $true) { + if ((Test-Path -Path $regKeyPath) -eq $false) { + New-Item -Path $regKeyPath + } + $completeString = $rebuildString + $clrString + "=" + $keyValue + Write-Host "$loglead : Writing string $completeString to $regKeyPath" + Set-ItemProperty -Path $regKeyPath -Name "Environment" -Value ([string[]]($completeString)) -Force + } else { + Write-Host "$loglead : Registry key already set, skipping update to key." + } + } else { + Write-Host "$loglead : Runtime of $($packageManifest.ServiceManifest.runtime) for service [$Name] is not dot net core, skipping registry." + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.tests.ps1 new file mode 100644 index 0000000..957250e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-DotNetCoreProfiling.tests.ps1 @@ -0,0 +1,135 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-DotNetCoreProfiling" { + + $global:manifestTemplate = @" + + + + _replaceME_ + + +"@ + $serviceName = "Alkami.Services.Tenant" + $ServiceRegkey = "HKLM:\SYSTEM\CurrentControlSet\Services" + $serviceDirPath = "C:\ProgramData\chocolatey\lib-bad\$serviceName" + + # Mock the pieces that make sure that the app.config is a real file. + Mock -ModuleName $moduleForMock New-item {} + Mock -ModuleName $moduleForMock Get-LogLeadName {} + Mock -ModuleName $moduleForMock Get-ChocolateyInstallPath {} + Mock -ModuleName $moduleForMock Set-ItemProperty {} + Mock -ModuleName $moduleForMock Write-Host {} + Mock -ModuleName $moduleForMock Get-PackageManifest {return ""} + Mock -ModuleName $moduleForMock Write-Warning {} + Mock -ModuleName $moduleForMock Test-IsServiceManifestCore { return $true } + Mock -ModuleName $moduleForMock Get-ItemProperty { } + + Context "Service is not dot net core" { + Mock -ModuleName $moduleForMock Test-IsServiceManifestCore { return $false} + + It "registry work is skipped" { + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Write-Host -Times 1 -Exactly -Scope It -ParameterFilter { + $object -eq " : Runtime of for service [$serviceName] is not dot net core, skipping registry." + } + } + } + Context "Reg key IS NOT present" { + Mock -ModuleName $moduleForMock Test-Path { return $false } + + It "If enabled is false, still creates Key" { + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock New-Item -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" + } + } + It "If enabled is true, still creates Key" { + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock New-Item -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" + } + } + It "Sets value to false" { + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq "CORECLR_ENABLE_PROFILING=0" + } + } + It "Sets value to true" { + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq "CORECLR_ENABLE_PROFILING=1" + } + } + } + Context "Reg key IS present" { + Mock -ModuleName $moduleForMock Test-Path { return $true } + + It "value already set to false, skips Setting" { + Mock -ModuleName $moduleForMock Get-ItemProperty { return @{Environment = @("CORECLR_ENABLE_PROFILING=0") } } + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Write-Host -Times 1 -Exactly -Scope It -ParameterFilter { + $object -eq " : Registry key already set, skipping update to key." + } + } + It "value already set to true, skips Setting" { + Mock -ModuleName $moduleForMock Get-ItemProperty { return @{Environment = @("CORECLR_ENABLE_PROFILING=1") } } + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock Write-Host -Times 1 -Exactly -Scope It -ParameterFilter { + $object -eq " : Registry key already set, skipping update to key." + } + } + } + Context "Reg key IS present, and value has contents" { + Mock -ModuleName $moduleForMock Get-ItemProperty { return @{Environment = @("george=best", "learning is FUNdamental") } } + Mock -ModuleName $moduleForMock Test-Path { return $true } + + It "value is not pre-set and is set to false" { + [string[]]$expectedEnvValue = "george=best" + $([char]0) + "learning is FUNdamental" + $([char]0) + "CORECLR_ENABLE_PROFILING=0" + $([char]0) + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq $expectedEnvValue + } + } + It "value is not pre-set and is set to true" { + [string[]]$expectedEnvValue = "george=best" + $([char]0) + "learning is FUNdamental" + $([char]0) + "CORECLR_ENABLE_PROFILING=1" + $([char]0) + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq $expectedEnvValue + } + } + } + Context "Reg key IS present, value has contents, and setting is flipped" { + Mock -ModuleName $moduleForMock Test-Path { return $true } + + It "value is true and is set to false" { + Mock -ModuleName $moduleForMock Get-ItemProperty { return @{Environment = @("george=best", "CORECLR_ENABLE_PROFILING=1", "learning is FUNdamental") } } + [string[]]$expectedEnvValue = "george=best" + $([char]0) + "learning is FUNdamental" + $([char]0) + "CORECLR_ENABLE_PROFILING=0" + $([char]0) + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $false + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq $expectedEnvValue + } + } + It "value is false and is set to true" { + Mock -ModuleName $moduleForMock Get-ItemProperty { return @{Environment = @("george=best", "CORECLR_ENABLE_PROFILING=0", "learning is FUNdamental") } } + [string[]]$expectedEnvValue = "george=best" + $([char]0) + "learning is FUNdamental" + $([char]0) + "CORECLR_ENABLE_PROFILING=1" + $([char]0) + + Set-DotNetCoreProfiling -Path $serviceDirPath -Enabled $true + Assert-MockCalled -ModuleName $moduleForMock Set-ItemProperty -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -eq "$ServiceRegkey\$serviceName" -and $Name -eq "Environment" -and $Value -eq $expectedEnvValue + } + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.ps1 new file mode 100644 index 0000000..5335d95 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.ps1 @@ -0,0 +1,30 @@ +function Set-ServiceAccountManagedState { +<# +.SYNOPSIS + Set the service managed account state value to True + While we could provide a flag to set it to off, we don't do that, because we use gMSA accounts + +.PARAMETER ServiceName + The service name to set the state for +#> + [CmdletBinding()] + param ( + [string]$ServiceName + ) + + $logLead = (Get-logLeadName) + + if ($null -eq (Get-Service -Name $ServiceName)) { + Write-Warning "$logLead : [$ServiceName] does not appear to be a valid service. Can not set the service account managed state" + return + } + + Write-Host "$logLead : Setting process to managedaccount $ServiceName true" + Invoke-SCExe @("managedaccount",$ServiceName,"True") + + if ($LASTEXITCODE -ne 0) { + Write-Error "$logLead : Attempt to call sc.exe and set service account managed state errored out. Please review logs" + } else { + Write-Host "$logLead : Successfully set the managed state for [$ServiceName]" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.tests.ps1 new file mode 100644 index 0000000..093c727 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceAccountManagedState.tests.ps1 @@ -0,0 +1,50 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-ServiceAccountManagedState" { + + Context "Ensure that it returns with a warning for no service returned" { + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return $null } + + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { $global:LASTEXITCODE = 1 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + + Mock -ModuleName $moduleForMock -CommandName Write-Warning + + Set-ServiceAccountManagedState "anything" + + It "Ensures Write-Warning got called once for `$null service" { + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope Context -ModuleName $moduleForMock + } + + It "Ensures Write-Error did not get called for `$null service" { + Assert-MockCalled -CommandName Write-Error -Times 0 -Scope Context -ModuleName $moduleForMock + } + } + + Context "Ensure that it returns an error for a non-zero exit code" { + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return @{}; } #return a non-null value + + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { $global:LASTEXITCODE = 1 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + + Mock -ModuleName $moduleForMock -CommandName Write-Warning + + Set-ServiceAccountManagedState "anything" + + It "Ensures Write-Warning got called once for `$null service" { + Assert-MockCalled -CommandName Write-Warning -Times 0 -Scope Context -ModuleName $moduleForMock + } + + It "Ensures Write-Error did not get called for `$null service" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Scope Context -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.ps1 new file mode 100644 index 0000000..0974d11 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.ps1 @@ -0,0 +1,62 @@ + +function Set-ServiceRecoveryOneRestart { +<# +.SYNOPSIS + Set the service with specified name to restart once, then no more recovery actions + +.PARAMETER ServiceName + The service name to set the state for +#> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions","",Justification="Alkami does not typically check for should-proceed for intentionally state changing operations")] + [CmdletBinding()] + param ( + [string]$ServiceName + ) + + $logLead = (Get-logLeadName) + + if ($null -eq (Get-Service -Name $ServiceName)) { + Write-Warning "$logLead : [$ServiceName] does not appear to be a valid service. Can not set the service recovery restart" + return + } + +<# +from the sc.exe failure configuration options message: + +DESCRIPTION: + Changes the actions upon failure +USAGE: + sc failure [service name] ... + +OPTIONS: + reset= + (Must be used in conjunction with actions= ) + reboot= + command= + actions= > + (Must be used in conjunction with the reset= option) +#> + # action/time//// + # This sets the first one to be restart after 10s, then nothing else + $recoveryActions = "restart/10000////" + # 86400 = default (24 hours) + + Write-Host "$logLead : Setting recovery actions for [$ServiceName]" + Invoke-SCExe @("failure",$ServiceName,"actions=",$recoveryActions,"reset=",86400) + + if ($LASTEXITCODE -ne 0) { + Write-Error "$logLead : Attempt to call sc.exe and set failure recovery actions errored out. Please review logs" + } else { + Write-Host "$logLead : Recovery options set successfully for [$ServiceName]" + } + + Write-Host "$logLead : Setting failureflag to 1" + Invoke-SCExe @("failureflag",$ServiceName,"1") + + if ($LASTEXITCODE -ne 0) { + Write-Error ("$logLead : An error occurred setting the failure flag option for service $ServiceName") + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.tests.ps1 new file mode 100644 index 0000000..7c771f0 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-ServiceRecoveryOneRestart.tests.ps1 @@ -0,0 +1,114 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Set-ServiceRecoveryOneRestart" { + + Context "Ensure that it returns with a warning for no service returned" { + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return $null } + + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { $global:LASTEXITCODE = 1 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + + Mock -ModuleName $moduleForMock -CommandName Write-Warning + + Set-ServiceRecoveryOneRestart "anything" + + It "Ensures Write-Warning got called once for `$null service" { + Assert-MockCalled -CommandName Write-Warning -Times 1 -Scope Context -ModuleName $moduleForMock + } + + It "Ensures Write-Error did not get called for `$null service" { + Assert-MockCalled -CommandName Write-Error -Times 0 -Scope Context -ModuleName $moduleForMock + } + } + + Context "Ensure that it returns an error for a non-zero exit code" { + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return @{}; } #return a non-null value + + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { $global:LASTEXITCODE = 1 } + + Mock -ModuleName $moduleForMock -CommandName Write-Error + + Mock -ModuleName $moduleForMock -CommandName Write-Warning + + Set-ServiceRecoveryOneRestart "anything" + + It "Ensures Write-Warning did not get called for `$null service" { + Assert-MockCalled -CommandName Write-Warning -Times 0 -Scope Context -ModuleName $moduleForMock + } + + It "Ensures Write-Error got called twice for `$null service" { + Assert-MockCalled -CommandName Write-Error -Times 1 -Scope Context -ModuleName $moduleForMock -ParameterFilter {$Message -match "Attempt to call sc.exe"} + Assert-MockCalled -CommandName Write-Error -Times 1 -Scope Context -ModuleName $moduleForMock -ParameterFilter {$Message -match "An error occurred setting" } + } + } + + # The only really useful thing we can check is to make sure we didn't shoot ourselves in the foot with a bad parameter. + Context "Ensure that the parameters being passed to sc.exe match what we need them to be" { + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { return @{}; } #return a non-null value + + $global:MagicValue = @() + + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { + if ($global:MagicValue.Length -eq 0) { + $global:MagicValue = $args + $global:LASTEXITCODE = 0 + } + } + + Mock -ModuleName $moduleForMock -CommandName Write-Error -MockWith {} + + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Set-ServiceRecoveryOneRestart "anything" + + $testArray = $global:MagicValue -split ' ' + # failure anything actions= restart/10000//// reset= 86400 + $testSpecial = $testArray[4] # -> restart/10000//// + # When you are debugging this and want to see it fail, ues this test case + # Leaving this here in case I need to understand what broke later + # $testSpecial = "restart/10000///anything-but-run-restart-reboot/NaN" + + $testComponents = $testSpecial -split '/' + $evenCount = 0; + $oddCount = 0; + for ($i = 0; $i -lt $testComponents.Length; $i++) { + $component = $testComponents[$i] + if (($i % 2) -eq 0) { + # evens + It "Verifies that the even numbered parameters are correctly parseable as run/restart/reboot/empty" { + $component | Should -BeIn @('', 'run', 'restart', 'reboot') -Because "Position [$i] was [$component]" + } + $evenCount++ + } else { + # odds + It "Verifies that the odd numbered parameters are correctly parseable as digits" { + $component | Should -Match '^[0-9]*$' -Because "Position [$i] was [$component]" + } + $oddCount++ + } + } + $evenCount | Should -BeGreaterThan 0 + $evenCount | Should -MatchExactly $oddCount + + Remove-Variable -Scope Global -Name MagicValue + + It "Ensures Write-Warning did not get called for best path" { + Assert-MockCalled -CommandName Write-Warning -Times 0 -Scope Context -ModuleName $moduleForMock + } + + It "Ensures Write-Error did not get called for best path" { + Assert-MockCalled -CommandName Write-Error -Times 0 -Scope Context -ModuleName $moduleForMock + } + + It "Calls Invoke-SCExe Twice" { + Assert-MockCalled -CommandName Invoke-SCExe -Times 2 -Scope Context -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Set-WindowsServiceExecutionAccount.ps1 b/Modules/Alkami.PowerShell.Services/Public/Set-WindowsServiceExecutionAccount.ps1 new file mode 100644 index 0000000..227fed9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Set-WindowsServiceExecutionAccount.ps1 @@ -0,0 +1,85 @@ +function Set-WindowsServiceExecutionAccount { +<# +.SYNOPSIS + Sets the Execution Account for a Windows Service + +.PARAMETER ServiceDefinition + A complex object with the following properties: Name, User, Password, IsGMSAAccount + Name is the name of the service + +.PARAMETER ServiceName + The name of the service. Used with Get-Service et al + +.PARAMETER ServiceUser + The user the service will run under + +.PARAMETER ServicePassword + The password for the service (if supplied) + +.PARAMETER IsGMSAAccount + If this is a gMSA service account +#> + [CmdletBinding(DefaultParameterSetName = 'ServiceDefinition')] + Param( + [Parameter(ParameterSetName = 'ServiceDefinition', Mandatory = $true)] + [PSObject]$ServiceDefinition, + + [Parameter(ParameterSetName = 'FieldBasedDefinition', Mandatory = $true)] + [string]$ServiceName, + [Parameter(ParameterSetName = 'FieldBasedDefinition', Mandatory = $true)] + [string]$ServiceUser, + [Parameter(ParameterSetName = 'FieldBasedDefinition', Mandatory = $false)] + [string]$ServicePassword, + [Parameter(ParameterSetName = 'FieldBasedDefinition')] + [switch]$IsGMSAAccount + ) + + $logLead = (Get-LogLeadName) + + if (($ServiceUser -eq 'REPLACEME') -or (($ServicePassword -eq 'REPLACEME') -and -not $IsGMSAAccount)) { + Write-Warning "$logLead : Service username or service password provided was [REPLACEME]. This is an invalid configuration. Set-WindowsServiceExecutionAccount will not be processed." + return + } + + if ($PSCmdlet.ParameterSetName -eq 'ServiceDefinition') { + $ServiceName = $ServiceDefinition.Name + $ServiceUser = $ServiceDefinition.User + $ServicePassword = $ServiceDefinition.Password + $IsGMSAAccount = $ServiceDefinition.IsGMSAAccount + } + + $emptyPassword = ([string]::IsNullOrWhiteSpace($ServicePassword)) + + $currentProcessUser = Get-WindowsServiceUser $ServiceName + + if ($currentProcessUser -eq $ServiceUser -or ($currentProcessUser -eq "LocalSystem" -and $ServiceUser -eq "SYSTEM")) { + Write-Host "$logLead : No Credential Update Required for Windows Service [$ServiceName]" + return + } + + $scParameters = @("config",$ServiceName,"obj=`"$ServiceUser`"") + + # GMSA don't have passwords, so don't specify that flag + if ($IsGMSAAccount) { + Write-Host "$logLead : Service [$ServiceName] will run as a GMSA account or Password-less Account for username [$ServiceUser]" + } + + if (!$IsGMSAAccount -and !$emptyPassword){ + Write-Host "$logLead : Service [$ServiceName] will run as a non-GMSA account" + $scParameters += "password=`"$ServicePassword`"" + } + + # This could potentially run with secure information if a password is provided + if ($emptyPassword) { + Write-Host "$logLead : Updating Execution Account for Windows Service [$ServiceName] with params [$scParameters]" + } else { + Write-Host "$logLead : Updating Execution Account for Windows Service [$ServiceName] with obscured params due to inclusion of a password" + } + + Invoke-SCExe $scParameters + + if ($IsGMSAAccount) { + # Ensure value is always set to service account managed state + Set-ServiceAccountManagedState -ServiceName $ServiceName + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-AlkamiService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-AlkamiService.ps1 new file mode 100644 index 0000000..a3f6f3f --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-AlkamiService.ps1 @@ -0,0 +1,170 @@ +function Start-AlkamiService { + <# + .SYNOPSIS + Starts a service, and throws an error if the service does not start in -Timeout seconds. + + .PARAMETER ServiceName + The name of the service to start + + .PARAMETER Timeout + The maximum number of seconds to wait. Defaults to 60 seconds + + .PARAMETER StatusUpdateIntervalSeconds + Number of seconds to wait between log statements during the "Timeout Loop" when waiting for the service to start. This is to avoid having 60 log statements for each service start attempt that times out + + .PARAMETER Force + Ignore whether or not Get-Service returns a service for ServiceName, try to start it anyway. Primarily useful for testing. + #> + + Param( + [Parameter(Mandatory = $true)] + [string]$ServiceName, + [Parameter(Mandatory = $false)] + [int]$Timeout = 60, + [Parameter(Mandatory = $false)] + [int]$StatusUpdateIntervalSeconds = 10, + [Parameter(Mandatory = $false)] + [switch]$Force + ) + + #region entry-guards + $logLead = Get-LogLeadName + $runningConstant = "Running" + $disabledConstant = "Disabled" + $startPendingConstant = "StartPending" + + if ($Force) { + Write-Warning "$logLead : Force flag present! Unexepected behavior may occur AND log statements may not always be accurate, especially regarding existence or state of the service named [$ServiceName]" + } + + Write-Host "$logLead : Get-Service -Name [$ServiceName]" + + try { + $service = Get-Service -Name $ServiceName + + } catch { + $caughtEx = $_ + Write-Warning "$loglead : Service with name $ServiceName not found" + if ($Force) { + Write-Host "$loglead : Force flag enabled, continuing" + } else { + throw $caughtEx + } + } + + if ($null -eq $service -and -not $Force) { + Write-Warning "$logLead : Service [$ServiceName] was not found" + + return + } + + if ($service.StartType -eq $disabledConstant) { + Write-Warning "$logLead : Service [$ServiceName] was disabled" + return + } + + if ($service.Status -eq $runningConstant) { + Write-Host "$logLead : Service [$ServiceName] is already running." + return + } + + if ([int]$Timeout -le 0) { + Write-Warning "$logLead : Invalid Timeout value [$Timeout], defaulting to 60" + $Timeout = 60 + } + #endregion entry-guards + $startTime = Get-Date + Write-Host "$logLead : Starting [$ServiceName] at [$startTime]" + + # Invoke Command With Retry timing. + $icwrTiming = @{ + MaxRetries = 3 + Seconds = 3 + JitterMin = -500 + JitterMax = 1500 + } + + $icwrArgs = @{ + Timeout = $Timeout + StatusUpdateIntervalSeconds = $StatusUpdateIntervalSeconds + LogLead = $logLead + RunningConstant = $runningConstant + StartPendingConstant = $startPendingConstant + } + $scriptblockStartService = { + param($sbServiceName, $sbInputArgs) + $sbTimeout = $sbInputArgs.Timeout + $sbStatusUpdateIntervalSeconds = $sbInputArgs.StatusUpdateIntervalSeconds + $sbLogLead = "sb_$($sbInputArgs.LogLead)" + $sbRunningConstant = $sbInputArgs.RunningConstant + $sbStartPendingConstant = $sbInputArgs.StartPendingConstant + + $sbPriorSvcStatus = (Get-Service -Name $sbServiceName -ErrorAction Ignore).Status + if ($sbPriorSvcStatus -eq $sbRunningConstant) { + Write-Warning "$sbLogLead : Service [$sbServiceName] already running; maybe a previously timed out attempt succeeded during backoff period. Exiting..." + return + } + + if ($sbPriorSvcStatus -eq $sbStartPendingConstant) { + Write-Warning "$sbLoglead : service [$sbServiceName] is already in state [$sbStartPendingConstant]" + Write-Warning "$sbLogLead : Waiting for [$sbServiceName] to start instead of calling start command" + } else { + try { + Write-Host "$sbLogLead : start [$sbServiceName]" + Invoke-SCExe -Arguments @("start", $sbServiceName) + } catch { + # Is an error on start + $sbCaughtEx = $_ + Write-Warning "$sbLogLead : start [$sbServiceName] failed due to immediate start failure" + Write-Warning "$sbLogLead : $($sbCaughEx.Exception.Message)" + throw $sbCaughtEx + } + } + + # Slowly poll to make sure that the service is running. + Write-Host "$sbLogLead : waiting for [$sbServiceName] to enter state $sbRunningConstant (max $sbTimeout seconds)" + + $sbLastStatus = "" + + for ($i = 0; $i -lt $sbTimeout; $i++) { + $sbLastStatus = (Get-Service -Name $sbServiceName).Status + if ($sbLastStatus -eq $sbRunningConstant) { + Write-Host "$sbLogLead : Service [$sbServiceName] was started successfully" + return + } + + # The following condition ALWAYS means the service entered the RUNNING state AND THEN LEFT IT + # For explanations: + # See https://docs.microsoft.com/en-us/windows/win32/services/service-status-transitions + # See https://jira.alkami.com/browse/SRE-18231?focusedCommentId=5158719&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-5158719 + if ($sbLastStatus -ne $sbStartPendingConstant) { + Write-Host "$sbLogLead : Current status: [$sbLastStatus]" + Write-Host "$sbLogLead : Expected status: [$sbStartPendingConstant]" + + Write-Warning "$sbLogLead : [$sbServiceName] started but is not in status [$sbRunningConstant] - CHECK LOGS" + throw "$sbLogLead : Service [$sbServiceName] STOPPED RUNNING during the timeout period for some OTHER reason. Last status was [$sbLastStatus] - CHECK LOGS" + + } + + Start-Sleep -Seconds 1 + $sbShouldWriteStatus = ($i + 1) % $sbStatusUpdateIntervalSeconds -eq 0 + if ($sbShouldWriteStatus) { + Write-Host "$sbLogLead : still waiting to make sure [$sbServiceName] has started. Last status was [$sbLastStatus] - seconds elapsed - [$($i + 1)] of [$sbTimeout]" + } + + } + + Write-Host "$sbLogLead : start service [$sbServiceName] failed due to a TIMEOUT FAILURE" + + throw "$sbLogLead : Service [$sbServiceName] could not be started within the timeout period. Please investigate. Last status was [$sbLastStatus]" + } + + $icwrSplat = @{ + ScriptBlock = $scriptblockStartService + Arguments = @($ServiceName, $icwrArgs) + Exponential = $true + } + + Invoke-CommandWithRetry @icwrSplat @icwrTiming + +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.ps1 new file mode 100644 index 0000000..cf66813 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.ps1 @@ -0,0 +1,23 @@ +function Start-DependentServices { +<# +.SYNOPSIS + Deprecated methodology for starting the Subscription Service and Broker. Will be removed at a later date +#> + + $logLead = (Get-LogLeadName); + Write-Warning "$logLead : Deprecated method for returning Tier 0 MS. Replace with a call to Get-ChocolateyServicesToStart" + + $rawservicesToStart = @(Get-AlkamiServices) + + $dependentServicesToStart = @() + $dependentServicesToStart += @($rawservicesToStart | Where-Object { $_.Status -eq "Stopped" -and $_.Name -match "Broker" }) + $dependentServicesToStart += @($rawservicesToStart | Where-Object { $_.Status -eq "Stopped" -and $_.Name -match "Subscriptions" }) + + if (Test-IsCollectionNullOrEmpty $dependentServicesToStart) { + + Write-Host "$logLead : No Dependent Services Found to Start" + } else { + $serviceNamesToStart = $dependentServicesToStart | Select-Object -ExpandProperty "Name"; + Start-ServicesInParallel $serviceNamesToStart; + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.tests.ps1 new file mode 100644 index 0000000..c1dc005 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-DependentServices.tests.ps1 @@ -0,0 +1,28 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-DependentServices" { + + Context "Error Handling" { + + It "Does Not Throw if the Services to Start Collection is Null" { + + Mock -CommandName Get-AlkamiServices -MockWith { + + return $null + + } -ModuleName $moduleForMock + + Mock -CommandName Start-ServicesInParallel -MockWith {} -ModuleName $moduleForMock + + { Start-DependentServices } | Should -Not -Throw + Assert-MockCalled -CommandName Get-AlkamiServices -Times 1 -Scope It -ModuleName $moduleForMock + Assert-MockCalled -CommandName Start-ServicesInParallel -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.ps1 new file mode 100644 index 0000000..144fd3a --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.ps1 @@ -0,0 +1,71 @@ +function Start-FileBeatsService { +<# +.SYNOPSIS + Start the filebeats service on a given machine. + +.DESCRIPTION + Start the filebeats service on a given machine. This function has the side effect of clearing a registry file if needed. + +.PARAMETER ServiceName + Additional refining value for searching for the service. When not present, defaults to checking the paths returned by Get-FileBeatsPath + +.PARAMETER Timeout + How long to wait on startup. Defaults to 5 seconds + +.EXAMPLE + Start-FileBeatsService +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$ServiceName, + + $Timeout = 5 + ) + + $logLead = (Get-LogLeadName) + $filebeatServices = (Get-FileBeatsService -SearchPrefix $ServiceName) + + if (Test-IsCollectionNullOrEmpty $filebeatServices) { + Write-Warning "$logLead : Filebeats not installed. Nothing to do." + return + } + + foreach ($filebeatService in $filebeatServices) { + $ServiceName = $filebeatService.Name + + try { + (Start-AlkamiService -ServiceName $ServiceName -Timeout $Timeout) | Out-Null + Write-Host "$logLead : Service [$ServiceName] started successfully" + } catch { + # If anything throws in this catch, that's okay. We want to let the user know that we have an issue. + # This should be automated, so anything that's happening ought to go to TC logs + # I'm not try-catching tho because I want this to fail miserably if this can't do the thing + Write-Warning "$logLead : Service [$ServiceName] not started successfully, attempting intervention" + + Write-Host "$logLead : Ensuring service is stopped" + + (Stop-AlkamiService $ServiceName) | Out-Null + + # get the data path in the same folder as the exepath + $exePath = $filebeatService.ExePath + $dataPath = (Join-Path (Split-Path $exePath) "data") + + if (Test-Path $dataPath) { + Write-Host "$logLead : Clearing potentially corrupted files from $dataPath" + (Remove-FileSystemItem -Path $dataPath -Recurse -Force) | Out-Null + } else { + Write-Host "$logLead : Data path not found at [$dataPath]" + } + + # Ensure folder exists + (New-Item -Path $dataPath -ItemType Directory -Force) | Out-Null + + Write-Host "$logLead : Files cleared, attempting to start again" + + (Start-AlkamiService -ServiceName $ServiceName -Timeout $Timeout) | Out-Null + + Write-Host "$logLead : Service [$ServiceName] started successfully" + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.tests.ps1 new file mode 100644 index 0000000..628faca --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-FileBeatsService.tests.ps1 @@ -0,0 +1,123 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +<# +There are three potential use-cases in this exercise +We modify external state and the caller will respond differently depending on that external state +If nothing was ever wrong, we should see case #1 +In the case the third party is busted outside of our interaction, we should see case #2 +In the event that this function did the job it was intended to do, and everybody is happy, we should see case #3 + +Run first call second call +#1 pass +#2 throw throw +#3 throw pass +#> + +Describe "Start-FileBeatsService" { + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith { return "UUT" } + Mock -ModuleName $moduleForMock -CommandName Get-WindowsServiceApplicationPath -MockWith { return $env:TEMP } + Mock -ModuleName $moduleForMock -CommandName Stop-AlkamiService -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Remove-FileSystemItem -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Start-AlkamiService -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith { } + Mock -ModuleName $moduleForMock -CommandName Test-Path -MockWith { return $false } + Mock -ModuleName $moduleForMock -CommandName Split-Path -MockWith { return "dummy path" } + Mock -ModuleName $moduleForMock -CommandName Join-Path -MockWith { return "dummy path" } + Mock -ModuleName $moduleForMock -CommandName New-Item -MockWith { return $null } + + Context "Happiest Path" { + Mock -ModuleName $moduleForMock -CommandName Get-FileBeatsService -MockWith { + return @( @{ + Name = 'Filebeat (Haystack)' + DisplayName = 'Filebeat (Haystack)' + State = 'Running' + StartMode = 'Auto' + ExePath = '"C:\Tools\Beats\FileBeat\tools\\filebeat.exe"' + } ) + } + Mock -ModuleName $moduleForMock -CommandName Start-AlkamiService -MockWith { return $null } + + It "Exits cleanly with no throws" { + { Start-FileBeatsService } | Should -Not -Throw + } + } + + Context "Start-AlkamiService throws every time (worst path)" { + Mock -ModuleName $moduleForMock -CommandName Get-FileBeatsService -MockWith { + return @( @{ + Name = 'Filebeat (Haystack)' + DisplayName = 'Filebeat (Haystack)' + State = 'Running' + StartMode = 'Auto' + ExePath = '"C:\Tools\Beats\FileBeat\tools\\filebeat.exe"' + } ) + } + # assume the external program just hates us and fails every time no matter what we do + Mock -ModuleName $moduleForMock -CommandName Start-AlkamiService -MockWith { throw 'expected exception' } + + It "Tries to start service twice then throws an exception" { + { Start-FileBeatsService } | Should -Throw + Assert-MockCalled -CommandName Start-AlkamiService -ModuleName $moduleForMock -Times 2 -Exactly -Scope It + } + + } + + Context "Start-AlkamiService throws the first time but works the next time (expected path)" { + + Mock -ModuleName $moduleForMock -CommandName Get-FileBeatsService -MockWith { + return @( @{ + Name = 'Filebeat (Haystack)' + DisplayName = 'Filebeat (Haystack)' + State = 'Running' + StartMode = 'Auto' + ExePath = '"C:\Tools\Beats\FileBeat\tools\\filebeat.exe"' + }, + @{ + Name = 'Filebeat_os (Haystack)' + DisplayName = 'Filebeat_os (Haystack)' + State = 'Running' + StartMode = 'Auto' + ExePath = '"C:\Tools\Beats\FileBeat_os\tools\\filebeat.exe"' + }) + } + $script:firstPassCompleted = $false + $script:secondPassCompleted = $false + $script:thirdPassCompleted = $false + Mock -ModuleName $moduleForMock -CommandName Start-AlkamiService -MockWith { + # track external state as tho we were actively making changes to the full system instead of mocking things + # the actual target application will have two response states so we need to recognize that + if ($script:firstPassCompleted -eq $false) { + $script:firstPassCompleted = $true + Write-Host "PESTER: threw the first time" + throw 'expected exception' + } elseif ($script:firstPassCompleted -eq $true -and $script:secondPassCompleted -eq $false) { + $script:secondPassCompleted = $true + Write-Host "PESTER: did not throw the second time" + return + } elseif ($script:firstPassCompleted -eq $true -and $script:secondPassCompleted -eq $true -and $script:thirdPassCompleted -eq $false) { + $script:thirdPassCompleted = $true + Write-Host "PESTER: threw the third time" + throw 'expected exception' + } elseif ($script:firstPassCompleted -eq $true -and $script:secondPassCompleted -eq $true -and $script:thirdPassCompleted -eq $true) { + Write-Host "PESTER: did not throw the forth time" + return + } else { + Write-Host "PESTER: Found unexpected flag state, check brain" + return + } + } + + It "Tries to start each service twice for a total of four tries and does not throw an error" { + { Start-FileBeatsService } | Should -Not -Throw + Assert-MockCalled -CommandName Start-AlkamiService -ModuleName $moduleForMock -Times 4 -Exactly + } + + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.ps1 new file mode 100644 index 0000000..38f1d10 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.ps1 @@ -0,0 +1,70 @@ +function Start-ServicesChocolateyOnly { +<# +.SYNOPSIS + Starts Chocolatey services by tier. + +.DESCRIPTION + Starts Chocolatey services by tier. Maximum parallelism can be controlled with the maxParallel parameter + +.PARAMETER maxParallel + [int] Can be an int value from 1 to [int]::MaxValue. Limits service start parallelism + +.EXAMPLE +Start-ServicesChocolateyOnly + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. +[Get-ChocolateyServicesToStart] : Found 4 Chocolatey Services +[Start-ServicesChocolateyOnly] : Starting 2 Services in Tier 0 +[Start-ServicesInParallel] : Starting Service Alkami.Services.Subscriptions.Host +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Broker.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 0 took 00:00:27.3150594 to start +[Start-ServicesChocolateyOnly] : Starting 1 Services in Tier 1 +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Authorization.Service.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 1 took 00:00:14.5806190 to start +[Start-ServicesChocolateyOnly] : Starting 1 Services in Tier 2 +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Features.Beacon.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 2 took 00:00:13.4367245 to start +[Start-ServicesChocolateyOnly] : Done starting services. +#> + [CmdletBinding()] + [OutputType([void])] + Param( + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$maxParallel = 10 + ) + + $loglead = Get-LogLeadName + + [array]$stoppedChocolateyServices = Get-ChocolateyServicesToStart + + if (Test-IsCollectionNullOrEmpty -Collection $stoppedChocolateyServices) { + + Write-Warning "$logLead : No Chocolatey Services Found to Start" + return + } + + # This takes all stopped Choco services, groups them by the property Tier, then forces Tier as an Int to sort numerically ascending + # to enforce Tier 0 starts before Tier 1 before Tier 2, etc. + $groupedChocolateyServices = $stoppedChocolateyServices | Group-Object -Property Tier | Sort-Object @{e={$_.Name -as [int]}} + + foreach ($group in $groupedChocolateyServices) { + + $services = $group.Group | Select-Object -ExpandProperty ServiceName + + Write-Host "$logLead : Starting $($group.Count) Service(s) in Tier $($group.Name)" + $tierStopWatch = [System.Diagnostics.StopWatch]::StartNew() + Start-ServicesInParallel -serviceNamestoStart $services -maxParallel $maxParallel + Write-Host "$logLead : Tier $($group.Name) took $($tierStopWatch.Elapsed) to start" + $tierStopWatch.Stop() + } + + Write-Host "$loglead : Done starting services." +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.tests.ps1 new file mode 100644 index 0000000..4fe7ebf --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesChocolateyOnly.tests.ps1 @@ -0,0 +1,77 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-ServicesChocolateyOnly" { + + Context "Logic" { + + It "Writes a Warning and Returns If No Choco Services are Stopped" { + + Mock -CommandName Get-ChocolateyServicesToStart -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Start-ServicesInParallel -ModuleName $moduleForMock -MockWith {} + + Start-ServicesChocolateyOnly + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It ` + -ParameterFilter { $Message -match "No Chocolatey Services Found to Start"} + Assert-MockCalled -ModuleName $moduleForMock Start-ServicesInParallel -Times 0 -Exactly -Scope It + } + + It "Uses the Supplied MaxParallel Value" { + + Mock -CommandName Start-ServicesInParallel -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ChocolateyServicesToStart -ModuleName $moduleForMock -MockWith { + + return New-Object PSObject -Property @{ + ServiceName = "Alkami.Stopped.Service"; + ServicePath = "C:\BogusPath\Bogus.exe"; + Tier = "1"; + } + } + + Start-ServicesChocolateyOnly -MaxParallel 99 + Assert-MockCalled -ModuleName $moduleForMock Start-ServicesInParallel -Times 1 -Exactly -Scope It ` + -ParameterFilter { $maxParallel -eq 99} + } + + It "Calls Start Services Split by Tier" { + + Mock -CommandName Start-ServicesInParallel -ModuleName $moduleForMock -MockWith {} + Mock -CommandName Get-ChocolateyServicesToStart -ModuleName $moduleForMock -MockWith { + + $tier1 = New-Object PSObject -Property @{ + ServiceName = "Alkami.Tier1.Service"; + ServicePath = "C:\BogusPath\Bogus.exe"; + Tier = "1"; + } + + $tier2 = New-Object PSObject -Property @{ + ServiceName = "Alkami.Tier2.Service"; + ServicePath = "C:\BogusPath\Bogus.exe"; + Tier = "2"; + } + + $tier99 = New-Object PSObject -Property @{ + ServiceName = "Alkami.Tier99.Service"; + ServicePath = "C:\BogusPath\Bogus.exe"; + Tier = "99"; + } + + return @( $tier99, $tier2, $tier1 ) + } + + Start-ServicesChocolateyOnly + Assert-MockCalled -ModuleName $moduleForMock Start-ServicesInParallel -Times 1 -Exactly -Scope It ` + -ParameterFilter { [string]$serviceNamestoStart -eq "Alkami.Tier1.Service" } + Assert-MockCalled -ModuleName $moduleForMock Start-ServicesInParallel -Times 1 -Exactly -Scope It ` + -ParameterFilter { [string]$serviceNamestoStart -eq "Alkami.Tier2.Service" } + Assert-MockCalled -ModuleName $moduleForMock Start-ServicesInParallel -Times 1 -Exactly -Scope It ` + -ParameterFilter { [string]$serviceNamestoStart -eq "Alkami.Tier99.Service" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.ps1 new file mode 100644 index 0000000..95148c9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.ps1 @@ -0,0 +1,206 @@ +function Start-ServicesInParallel { +<# +.SYNOPSIS + Starts Multiple Windows Services in Parallel. + +.DESCRIPTION + Starts Multiple Windows Services in Parallel. + Max service start parallelism defaults to 0, which means to start services as quickly as possible. Limited by CPU capacity. + If max parallelism is greater than 0, the number of services that can start at a time will be limited to that many services at a time, in addition to limitation by CPU capacity. + +.PARAMETER ServiceNamestoStart + A string array of service names to start + +.PARAMETER MaxParallel + The maximum number of services to start in parallel. Defaults to 0, which starts every service as fast as possible. + +.PARAMETER MicroserviceCpuGuess + The estimate of how much CPU a microservice will utilize as it starts. Defaults to 16. + +.PARAMETER CpuTarget + The target maximum CPU percentage, as an Integer to use while starting services, in order to leave some overhead for the rest of the system. Defaults to 85. + Overridable via environment variable named 'ALKAMI_STARTSERVICESINPARALLEL_WITHTIMEOUTANDRETRY' + +.PARAMETER ReturnResults + Whether to return an array of result objects. Currently only exceptions. + +.PARAMETER WithoutTimeoutAndRetry + Causes the parallel jobs to use Start-Service instead of Start-AlkamiService. Start-AlkamiService has a default Timeout of 60 seconds and retries 3 times. + +.EXAMPLE + Start-ServicesInParallel -serviceNamesToStart @("Alkami Radium Scheduler Service", "Alkami Nag Service") -maxParallel 2 + +Starting Service Alkami Radium Scheduler Service +Starting Service Alkami Nag Service +.. +Done Starting Services +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Scope = 'Function', Justification = 'Per https://github.com/PowerShell/PSScriptAnalyzer/issues/1504 this is a known regression, skip this validation step')] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [string[]]$ServiceNamestoStart, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, [int]::MaxValue)] + [int]$MaxParallel = 0, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$MicroserviceCpuGuess = 16, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 100)] + [int]$CpuTarget = 85, + + [Parameter(Mandatory = $false)] + [switch]$ReturnResults, + + [Parameter(Mandatory = $false)] + [switch]$WithoutTimeoutAndRetry + ) + + $loglead = Get-LogLeadName + + # Return if there are no services to start. + if (Test-IsCollectionNullOrEmpty -Collection $ServiceNamestoStart) { + return + } + + # For each input object. + $jobs = @() + + $useStartAlkamiService = $true + $envVarWithTimeoutAndRetry = Get-EnvironmentVariable -Name "ALKAMI_STARTSERVICESINPARALLEL_WITHTIMEOUTANDRETRY" + + if ($WithoutTimeoutAndRetry -or $envVarWithTimeoutAndRetry -eq "false") { + $useStartAlkamiService = $false + } + + $envVarCpuTarget = Get-EnvironmentVariable -Name "ALKAMI_STARTSERVICESINPARALLEL_CPUTARGET" + if(!(Test-StringIsNullOrEmpty -Value $envVarCpuTarget)) { + # Get-EnvironmentVariable will return $null if the $env:ALKAMI_STARTSERVICESINPARALLEL_CPUTARGET has no value. + # Reusing the param $CpuTarget will still enforce the datatype and ValidateRange rules. + Write-Host "$loglead : Found environment variable 'ALKAMI_STARTSERVICESINPARALLEL_CPUTARGET' with a value of '$envVarCpuTarget'" + Write-Host "$loglead : Setting CpuTarget to $envVarCpuTarget" + + try { + $CpuTarget = $envVarCpuTarget + } catch { + Write-Warning "$loglead : Caught exception trying to set CpuTarget param from environment variable. Using default value '$CpuTarget'." + Write-Warning $_ + } + } + + Write-Host "$loglead : Using CpuTarget '$CpuTarget'" + + $serviceCounter = 0 # How many services we have started jobs for. + $numServicesToStart = 0 # The number of services in the 'queue' to start. + $errors = @() # Errors thrown from the service-starts. + do { + # If the max parallelism param is set and we are running too many jobs, wait for any job to complete. + if ( ($MaxParallel -gt 0) -and ($jobs.Count -ge $MaxParallel) ) { + (Wait-Job -Job $jobs -Any) | Out-Null + } + + # Scrub the jobs array of jobs that have finished, and receive their outputs. + $runningJobs = $jobs | Where-Object { ($_.State -eq "Running") -or ($_.State -eq "NotStarted") } + $completedJobs = $jobs | Where-Object { $_.State -eq "Completed" } + + if ( !(Test-IsCollectionNullOrEmpty -Collection $completedJobs) ) { + foreach ($completedJob in $completedJobs) { + try { + Receive-Job -Job $completedJob -ErrorAction Stop + } catch { + $errors += $_ + } + } + } + + [array]$jobs = $runningJobs + + # Figure out how many services we can start, if any. + if ($numServicesToStart -eq 0) { + + # Keep looping until we find the bandwidth to start another microservice. + while ($numServicesToStart -eq 0) { + $cpuUsage = Get-CPUUsage + $remainingCPU = $CpuTarget - $cpuUsage + + if ($remainingCPU -lt 0) { + $remainingCPU = 0 + } + + $extraMicroservicesToStart = $remainingCPU / $MicroserviceCpuGuess + $extraMicroservicesToStart = [Math]::Floor($extraMicroservicesToStart) + + if ($extraMicroservicesToStart -gt 0) { + $numServicesToStart += $extraMicroservicesToStart + } else { + Start-Sleep -Milliseconds 30 + } + } + + # Limit the number of services to start by the max parallelism param, if applicable. + if ($maxParallel -gt 0) { + $numStartableJobs = $maxParallel - $jobs.Count + + if ($numServicesToStart -gt $numStartableJobs) { + $numServicesToStart = $numStartableJobs + } + } + } + + # Get the service that we are starting, and decrement the services to start count. + $serviceName = $ServiceNamestoStart[$serviceCounter++] + $numServicesToStart-- + + # Start a new job. + $jobs += Start-Job -ArgumentList ($serviceName, $useStartAlkamiService, $logLead) -ScriptBlock { + param($sbServiceName, $sbUseStartAlkamiService, $sbLoglead) + Write-Host "$sbLogLead : Starting Service $sbServiceName" + Write-Host "$sbLogLead : UseStartAlkamiService is $sbUseStartAlkamiService" + if ($sbUseStartAlkamiService) { + Write-Host "$sbLogLead : Calling Start-AlkamiService..." + Start-AlkamiService -ServiceName $sbServiceName + Write-Host "$sbLogLead : Finished Start-AlkamiService" + } else { + Write-Host "$sbLogLead : Calling Start-Service..." + Start-Service -Name $sbServiceName -WarningAction SilentlyContinue + Write-Host "$sbLogLead : Finished Start-Service" + } + } + + } while ($serviceCounter -lt $ServiceNamestoStart.Count) + + # If there are outstanding jobs... + if ( !(Test-IsCollectionNullOrEmpty $jobs) ) { + + # Wait for all outstanding jobs to complete. + (Wait-Job -Job $jobs) | Out-Null + + # Receive all the jobs. + foreach($job in $jobs) { + try { + Receive-Job -Job $job -ErrorAction Stop + } catch { + $errors += $_ + } + } + } + + # Report if there were errors. + if ( !(Test-IsCollectionNullOrEmpty $errors) ) { + $errorString = $errors -join "`n" + # TODO: Evaluate risk of making this function fail. + # throw "$loglead There were issues starting microservices. Errors:`n$errorString" + Write-Warning "$loglead : There were issues starting microservices. Errors:`n$errorString" + } else { + Write-Host "`n$loglead : Done Starting Services" + } + + if ($ReturnResults) { + return $errors + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.tests.ps1 new file mode 100644 index 0000000..4fb54f4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesInParallel.tests.ps1 @@ -0,0 +1,38 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Start-ServicesInParallel + +Describe "Start-ServicesInParallel" { + + $testServiceNames = @("TestService1" , "TestService2") + + Context "Parameter Validation" { + + Mock -CommandName Start-Job -MockWith { return $null } -ModuleName $moduleForMock + Mock -CommandName Start-Service -MockWith { } -ModuleName $moduleForMock + + It "Should Not Throw if maxParallel is 0" { + # Zero is pure CPU throttling. + { Start-ServicesInParallel -serviceNamestoStart $testServiceNames -maxParallel 0 } | Should -Not -Throw + } + + It "Should Throw if maxParallel is less 1" { + { Start-ServicesInParallel -serviceNamestoStart $testServiceNames -maxParallel -1 } | Should -Throw + } + + It "Does Not Throw if maxParallel is greater than or equal to 1" { + { Start-ServicesInParallel -serviceNamestoStart $testServiceNames -maxParallel 1 } | Should -Not -Throw + { Start-ServicesInParallel -serviceNamestoStart $testServiceNames -maxParallel 50 } | Should -Not -Throw + } + + # Should ideally test that the runspace is created with the specified job ceiling, but cannot at present without abstraction + } + } + +#endregion Start-ServicesInParallel \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.ps1 new file mode 100644 index 0000000..e4178cd --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.ps1 @@ -0,0 +1,61 @@ +function Start-ServicesOnly { +<# +.SYNOPSIS + Starts any stopped Alkami Windows services, both Chocolatey based and Legacy + +.DESCRIPTION + Starts any stopped Alkami Windows services, both Chocolatey based and Legacy. Maximum parallelism can be controlled with the maxParallel parameter + +.PARAMETER maxParallel + Can be an int value from 1 to [int]::MaxValue. Limits service start parallelism + +.EXAMPLE +Start-ServicesOnly + +[Get-ChocolateyServices] : Finding services installed out of the chocolatey path: C:\ProgramData\chocolatey +[Get-ChocolateyServices] : Found 4 chocolatey services. +[Get-ChocolateyServicesToStart] : Found 4 Chocolatey Services +[Start-ServicesChocolateyOnly] : Starting 2 Services in Tier 0 +[Start-ServicesInParallel] : Starting Service Alkami.Services.Subscriptions.Host +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Broker.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 0 took 00:00:10.2552499 to start +[Start-ServicesChocolateyOnly] : Starting 1 Services in Tier 1 +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Authorization.Service.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 1 took 00:00:14.4166193 to start +[Start-ServicesChocolateyOnly] : Starting 1 Services in Tier 2 +[Start-ServicesInParallel] : Starting Service Alkami.MicroServices.Features.Beacon.Host + +[Start-ServicesInParallel] : Done Starting Services +[Start-ServicesChocolateyOnly] : Tier 2 took 00:00:13.5352469 to start +[Start-ServicesChocolateyOnly] : Done starting services. +[Start-ServicesInParallel] : Starting Service Alkami Radium Scheduler Service +[Start-ServicesInParallel] : Starting Service Filebeat (Haystack) + +[Start-ServicesInParallel] : Done Starting Services +#> + [CmdletBinding()] + [OutputType([void])] + Param( + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$maxParallel = 10 + ) + + $loglead = Get-LogLeadName + Start-ServicesChocolateyOnly -maxParallel $maxParallel + [array]$services = Get-ServicesToStart -SkipChocolateyServices + + Start-FileBeatsService + + if (Test-IsCollectionNullOrEmpty -Collection $services) { + + Write-Warning "$logLead : No Legacy Services Found to Start" + return + } + + Start-ServicesInParallel -ServiceNamestoStart $services -MaxParallel $maxParallel +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.tests.ps1 new file mode 100644 index 0000000..2f24617 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Start-ServicesOnly.tests.ps1 @@ -0,0 +1,64 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Start-ServicesOnly" { + + Mock -ModuleName $moduleForMock -CommandName Start-IISOnly -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-DependentServices -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-Sleep -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-FileBeatsService -MockWith {} + + Context "Error Handling" { + + It "Does Not Throw if the Services to Start Collection is Null" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServicesToStart -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServicesToStart -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServices -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiServices -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Start-ServicesInParallel -MockWith {} + + { Start-ServicesOnly } | Should -Not -Throw + Assert-MockCalled -CommandName Start-ServicesInParallel -Times 0 -Exactly -Scope It -ModuleName $moduleForMock + } + + It "Writes a Warning if the Services to Start Collection is Null" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServicesToStart -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServicesToStart -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServices -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiServices -MockWith { return $null } + Mock -ModuleName $moduleForMock -CommandName Start-ServicesInParallel -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + Start-ServicesOnly + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It -ModuleName $moduleForMock ` + -ParameterFilter { $Message -match "No Legacy Services Found to Start" } + } + } + + Context "Parameter Validation" { + + It "Uses the Supplied Max Parallel Parameter" { + + Mock -ModuleName $moduleForMock -CommandName Get-ServicesToStart -MockWith { return @("Totally Fake Legacy Service") } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServicesToStart -MockWith { return @("Totally Fake Choco Service") } + Mock -ModuleName $moduleForMock -CommandName Get-ChocolateyServices -MockWith { return @("Totally Fake Choco Service") } + Mock -ModuleName $moduleForMock -CommandName Get-AlkamiServices -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-ServicesInParallel -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Start-ServicesChocolateyOnly -MockWith {} + + $maxParallelTestValue = 20 + Start-ServicesOnly -maxParallel $maxParallelTestValue + Assert-MockCalled -CommandName Start-ServicesInParallel -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $maxParallel -eq $maxParallelTestValue } + Assert-MockCalled -CommandName Start-ServicesChocolateyOnly -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $maxParallel -eq $maxParallelTestValue } + } + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.ps1 new file mode 100644 index 0000000..78dc927 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.ps1 @@ -0,0 +1,113 @@ +function Stop-AlkamiService { + <# +.SYNOPSIS + Attempts to stop a service and kills the process if it doesn't complete shutdown within a configurable limit + +.PARAMETER ServiceName + Name of the service to stop + +.PARAMETER ServiceTimeoutSeconds + Number of seconds to wait for the service to stop +#> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $true)] + [Alias("sName")] + [string]$ServiceName, + + [Parameter(Mandatory = $false)] + [Alias("Seconds")] + [int]$ServiceTimeoutSeconds = 15 + ) + + $logLead = (Get-LogLeadName) + $serviceTimeout = [System.TimeSpan]::FromSeconds($ServiceTimeoutSeconds) + try { + $service = Get-Service -Name $ServiceName | Select-Object -First 1 + } catch { + Write-Warning "$logLead : $_" + } + + if ($null -eq $service -or $service.Status -eq [ServiceProcess.ServiceControllerStatus]::Stopped) { + Write-Host ("$logLead : Service {0} not found or was already stopped" -f $ServiceName) + return $true + } + + try { + $processes = @(Get-ProcessFromService $service) + if ($processes.Count -eq 0) { + Write-Host "$logLead : No running processes found for $($service.Name)" + return $true + } + + $process = ($processes | Select-Object -First 1) + Write-Host "$logLead : Stopping PID: $($process.Id) for [$($process.ProcessName)] at [$($process.path)]" + + # Warn about having too many processes + $otherProcesses = ($processes | Select-Object -Skip 1) + if ($otherProcesses.Count -gt 0) { + Write-Warning "$logLead : Found multiple processes for this service information. Please investigate." + Write-Host "$logLead : Only processing for the first process returned in the list" + foreach ($extraProcess in $otherProcesses) { + Write-Warning "$logLead : NOT STOPPING: PID: $($process.Id) for [$($process.ProcessName)] at [$($process.path)]" + } + } + } catch { + Write-Warning ("$logLead : {0}" -f ($Error[0] | Format-List -Force)) + } + + $service.Refresh() + if ($PSCmdlet.ShouldProcess($ServiceName, "Stopping Service")) { + if ($service.Status -ne "StopPending" -and $service.CanStop -eq $true) { + try { + Invoke-SCExe @("stop", $serviceName) + } catch { + Write-Warning ("$logLead : {0}" -f ($Error[0] | Format-List -Force)) + } + } + } + + try { + if ($PSCmdlet.ShouldProcess($ServiceName, "Wait for Service Stop")) { + $service.WaitForStatus([ServiceProcess.ServiceControllerStatus]::Stopped, $serviceTimeout) + $waitInterval = 0 + + do { + Start-Sleep -Seconds 1 + $waitInterval++ + } while ($null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue) -and ($waitInterval -lt $serviceTimeout.Seconds)) + + + # If the timeout expires, the catch block will handle killing the service + # If the service stops gracefully but the process doesn't quit, we'll kill it here + if ($null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue)) { + try { + Write-Warning ("$logLead : Old process still running after timeout. Killing process with ID {0}" -f $process.Id) + Stop-ProcessIfFound $process.Name + } catch { + # Do nothing, the process terminated before we could kill it + Write-Host "$logLead : The process terminated before we could kill it." + } + } + } + } + catch [ServiceProcess.TimeoutException] { + if ($null -ne $process.Id -or $process.Count -gt 0) { + Write-Warning ("$logLead : Timeout stopping service {0} after {1} seconds. The process will be killed." -f $ServiceName, $serviceTimeout.Seconds) + Stop-ProcessIfFound $process.Name + } else { + Write-Warning ("$logLead : The process ID could not be determined before shutdown.") + } + } + + # Check one more time if the service is stopped. + #$service = Get-Service -Name $ServiceName | Select-Object -First 1 + $service.Refresh() + $stopped = $service.Status -eq "Stopped" + if ($PSCmdlet.ShouldProcess($ServiceName, "Determining Return Value")) { + return $stopped + } else { + return $true + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.tests.ps1 new file mode 100644 index 0000000..4746319 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-AlkamiService.tests.ps1 @@ -0,0 +1,50 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +Describe "Stop-AlkamiService" { + + Mock -ModuleName $moduleForMock -CommandName Get-LogLeadName -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Get-Service -MockWith { + $returnObject = [PSCustomObject]@{Status='Running'; Name ='JunkService'} + $returnObject | Add-Member -MemberType ScriptMethod -Name Refresh -Value {write-host "setting $($this.Status) to Stopped"; $this.Status = 'Stopped'} -Force + $returnObject | Add-Member -MemberType ScriptMethod -Name WaitForStatus -Value {} -Force + + return $returnObject + } + + Mock -ModuleName $moduleForMock -CommandName Get-ProcessFromService -MockWith {[PSCustomObject]@(@{Id = 123; ProcessName = "MyService"; Path = "Some\Path"})} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Stop-ProcessIfFound -MockWith {} + + Context "When Successfully Trying to Stop A Service"{ + It "Doesn't Throw"{ + {Stop-AlkamiService "junkService" } | Should -Not -Throw + } + + It "Stops"{ + $returnValue = Stop-AlkamiService "junkService" + + # Depending on the $PSCmdlet.ShouldProcess result, either Stopped or True is valid here. This should be set to just Stopped once we wrap ShouldProcess + $returnValue | Should -Be ('Stopped' -or $true) + } + } + + Context "When Invoke-SCExe throws"{ + Mock -ModuleName $moduleForMock -CommandName Invoke-SCExe -MockWith { throw } + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + + # This won't function until we wrap ShouldProcess + It "Writes A Warning" -skip { + Stop-AlkamiService "junkService" + + Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It ` + -ModuleName $moduleForMock -ParameterFilter { $Message -match "Service failed to stop" } + } + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesChocolateyOnly.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesChocolateyOnly.ps1 new file mode 100644 index 0000000..55f232c --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesChocolateyOnly.ps1 @@ -0,0 +1,23 @@ +function Stop-ServicesChocolateyOnly { +<# +.SYNOPSIS + Stops services installed in a chocolatey folder path. +#> + + [CmdletBinding()] + param() + + $loglead = (Get-LogLeadName); + + Write-Output "$loglead Started" + + $servicesToStop = Get-ServicesToStop -skipAlkamiServices + + if ($servicesToStop) + { + Stop-ServicesInParallel $servicesToStop + } + + Write-Output "$loglead Completed" +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.ps1 new file mode 100644 index 0000000..58bbaa2 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.ps1 @@ -0,0 +1,69 @@ +function Stop-ServicesInParallel { +<# +.SYNOPSIS + Attempts to stop a list of services in parallel. It will kill the services if they do not respond in time. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [array]$serviceNamestoStop, + + [Parameter(Mandatory=$false)] + [int]$maxParallel = 30 + ) + + $logLead = (Get-LogLeadName); + + if ($maxParallel -lt 1) { + Write-Warning "$logLead maxParallel was set to less than 1. Minimum number of threads is 1, setting maxParallel to 30"; + $maxParallel = 30; + } + + # Define the script to stop an individual service. + $serviceStopScript = { + param( + $serviceName + ) + + Write-Host "[Stop-ServicesInParallel] Stopping service $serviceName"; + $serviceStopped = Stop-AlkamiService -sName $serviceName; + + $result = @{ + Result = $serviceStopped + ServiceName = $serviceName + } + return $result; + } + + Write-Host "$loglead Stopping $($serviceNamesToStop.Count) services."; + + # Stop all the services in parallel. + $results = Invoke-Parallel -objects $serviceNamestoStop -script $serviceStopScript -numThreads $maxParallel; + + # Figure out which services did not stop. + $badResults = $results | Where-Object { $_.Result -eq $false }; + if(!(Test-IsCollectionNullOrEmpty $badResults)) { + Write-Warning "$logLead $($badResults.Count) services were not successfully stopped:" + foreach($service in $badResults) { + Write-Warning "$logLead`t$($service.ServiceName) was not stopped."; + } + + Write-Warning "$logLead Attempting to stop services again." + $serviceNamestoStop = $badResults | ForEach-Object { $_.ServiceName }; + $results = Invoke-Parallel -objects $serviceNamestoStop -script $serviceStopScript -numThreads $maxParallel; + } + + # Figure out which services did not stop (again) + $badResults = $results | Where-Object { $_.Result -eq $false }; + if(!(Test-IsCollectionNullOrEmpty $badResults)) { + $errorString = "$logLead Could not stop $($badResults.Count) services:"; + foreach($service in $badResults) { + $errorString += "`n$logLead`tCould not stop $($service.ServiceName)"; + } + Write-Error $errorString; + return; + } + + Write-Host "$logLead Services were successfully stopped."; +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.tests.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.tests.ps1 new file mode 100644 index 0000000..39ca04d --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesInParallel.tests.ps1 @@ -0,0 +1,36 @@ +. $PSScriptRoot\..\..\Load-PesterModules.ps1 +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.' +$functionPath = Join-Path -Path $here -ChildPath $sut +Write-Host "Overriding SUT: $functionPath" +Import-Module $functionPath -Force +$moduleForMock = "" + +#region Stop-ServicesInParallel + +Describe "Stop-ServicesInParallel" { + + $testServiceNames = @("TestService1" , "TestService2") + + Context "Parameter Validation" { + + Mock -ModuleName $moduleForMock -CommandName Write-Host -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Write-Warning -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Stop-Service -MockWith {} + Mock -ModuleName $moduleForMock -CommandName Invoke-Parallel -MockWith {} + + It "Should warn if max-parallel is less 1" { + Stop-ServicesInParallel -serviceNamestoStop $testServiceNames -maxParallel 0 + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 1 -Exactly -Scope It + } + + It "Writes no warning if max-parallel is greater than 1" { + Stop-ServicesInParallel -serviceNamestoStop $testServiceNames -maxParallel 1 + + Assert-MockCalled -ModuleName $moduleForMock Write-Warning -Times 0 -Exactly -Scope It + } + } +} + +#endregion Stop-ServicesInParallel \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesOnly.ps1 b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesOnly.ps1 new file mode 100644 index 0000000..a1732d6 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Stop-ServicesOnly.ps1 @@ -0,0 +1,22 @@ +function Stop-ServicesOnly { +<# +.SYNOPSIS + Stops Installed Alkami Windows Services +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [switch]$skipSubscriptionService + ) + + $logLead = (Get-LogLeadName); + + $servicesToStop = Get-ServicesToStop -skipChocolateyServices -skipSubscriptionService:$skipSubscriptionService.IsPresent; + + if ($servicesToStop) + { + Stop-ServicesInParallel $servicesToStop; + } + + Write-Output "$logLead : Completed" +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-AreCriticalNagJobsRunning.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-AreCriticalNagJobsRunning.ps1 new file mode 100644 index 0000000..b604ef8 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-AreCriticalNagJobsRunning.ps1 @@ -0,0 +1,93 @@ +function Test-AreCriticalNagJobsRunning { +<# +.SYNOPSIS + Checks Running Nag Jobs and returns whether or not it's safe to restart it. +#> + [CmdletBinding()] + [OutputType([System.Boolean])] + Param( + [Parameter(Mandatory = $false)] + [Alias("Minutes")] + [int]$threshold = 5, + [Parameter(Mandatory = $false)] + [Alias("ConnectionString")] + [string]$masterConnectionString + ) + + $logLead = (Get-LogLeadName); + + $utcNow = (Get-Date).ToUniversalTime() + Write-Verbose ("$logLead : utcNow is {0}" -f $utcNow) + $utcNowTicks = $utcNow.Ticks + Write-Verbose ("$logLead : utcNowTicks is {0}" -f $utcNowTicks) + $utcThreshold = (Get-Date).ToUniversalTime().AddMinutes($threshold) + Write-Verbose ("$logLead : utcThreshold is {0}" -f $utcThreshold) + $utcThresholdTicks = $utcThreshold.Ticks + Write-Verbose ("$logLead : utcThresholdTicks is {0}" -f $utcThresholdTicks) + + ## NAG_FIRED_TRIGGERS contains triggers in progress, and NAG_TRIGGERS contains scheduled triggers + ## This query will return those within the threshold or which are currently firing + + $resourcesPath = Join-Path $PSScriptRoot "Resources" + $triggerQueryPath = Join-Path $resourcesPath "TriggerQuery.sql" + $triggerQuery = Get-Content $triggerQueryPath -Raw + + Write-Verbose ("$logLead : Checking for running jobs and jobs scheduled to run before {0}" -f $utcThreshold.ToLocalTime()) + + if (!$masterConnectionString) { + $masterConnectionString = Get-MasterConnectionString + } + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + + $conn = New-Object System.Data.SqlClient.SqlConnection + $conn.ConnectionString = $conStrBuilder.ToString() + + $cmd = New-Object System.Data.SqlClient.SqlCommand($triggerQuery, $conn) + $cmd.Parameters.Add("@now", $utcNowTicks) | Out-Null + $cmd.Parameters.Add("@threshold", $utcThresholdTicks) | Out-Null + + try { + $conn.Open() + + $sqlReader = $cmd.ExecuteReader() + + if ($sqlReader.HasRows) { + while ($sqlReader.Read()) { + $fireTime = $sqlReader.GetInt64(0) + Write-Verbose ("$logLead : Fire ticks is {0}" -f $fireTime) + + $scheduledTime = $sqlReader.GetInt64(1) + Write-Verbose ("$logLead : Scheduled ticks is {0}" -f $scheduledTime) + + $jobName = $sqlReader.GetString(2) + + if ($scheduledTime -gt 0 -or $fireTime -gt 0) { + # This trigger/job is scheduled to execute within the threshold + $friendlyTime = (New-Object System.DateTime($fireTime)).ToLocalTime().ToString("MM-dd-yyyy hh:mm:ss") + $friendlyDateTime = [System.DateTime]::Parse($friendlyTime) + + if ($friendlyDateTime -gt (Get-Date)) { + # This trigger/job is currently executing + # This scheduledTime variable is the original scheduled execution time, so the names can be a bit confusing + $friendlyTime = (New-Object System.DateTime($scheduledTime)).ToLocalTime().ToString("MM-dd-yyyy hh:mm:ss") + Write-Warning ("$logLead : <{0}> began run at {1}" -f $jobName, $friendlyTime) + } else { + Write-Warning ("$logLead : <{0}> is scheduled to run at {1}" -f $jobName, $friendlyTime) + } + } + } + } else { + # No records found, we're good to recycle + Write-Verbose "$logLead : No tasks running and none scheduled before the cutoff threshold" + return $true + } + } finally { + # Cleanup the System.Data.SqlClient objects + if ($conn.State -ne [System.Data.ConnectionState]::Closed) { + $conn.Close() + } + + $conn = $null + } +} diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-IsNagRunning.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-IsNagRunning.ps1 new file mode 100644 index 0000000..61c5b60 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-IsNagRunning.ps1 @@ -0,0 +1,28 @@ +function Test-IsNagRunning { + <# +.SYNOPSIS + Tests if Nag is running on the specified server +.PARAMETER server + Server to test +#> + param( + [ValidateNotNullOrEmpty()] + [Alias("ComputerName")] + [string]$Server = "localhost" + ) + + $logLead = (Get-LogLeadName); + + try { + $remoteNagServiceInfo = Get-Service "Alkami Nag Service" -ComputerName $Server + } catch [Microsoft.PowerShell.Commands.ServiceCommandException] { + Write-Warning "$logLead : Nag was not found on $Server" + return $false + } + + if ($remoteNagServiceInfo.Status -match "Running") { + return $true + } else { + return $false + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-IsNagServer.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-IsNagServer.ps1 new file mode 100644 index 0000000..1bd7a20 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-IsNagServer.ps1 @@ -0,0 +1,31 @@ +function Test-IsNagServer { + <# +.SYNOPSIS + Tests if the specified server is the designated Nag server for this pod. +.PARAMETER server + Server to test +#> + param( + [ValidateNotNullOrEmpty()] + [Alias("ComputerName")] + [string]$Server = "localhost" + ) + # Set the path where the nag config file lives. + + $orbPath = Get-OrbPath + + $nagConfigFilepath = Join-Path $orbPath "\Nag\Alkami.App.Nag.Host.Service.exe.config" + if ((![string]::IsNullOrWhiteSpace($Server)) -and ($Server -ne "localhost")) { + $nagAppConfigPath = Get-UncPath -filePath $nagConfigFilepath -ComputerName $Server + } else { + $nagAppConfigPath = $nagConfigFilepath + } + + # blatant theft er "borrowing": https://stackoverflow.com/a/27485038/3691973 + # Yes, it's overkill. But it's concise. + $slaveNodeValue = Get-AppSetting -filePath $nagAppConfigPath -appSettingKey "Nag.IsSlaveNode" + + $isSlaveNode = switch ($slaveNodeValue) { { $_ -eq 1 -or $_ -eq "True" } { $true } default { $false } } + + return (!$isSlaveNode -and (Test-IsNagRunning -Server $Server)) +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-NagService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-NagService.ps1 new file mode 100644 index 0000000..3db6458 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-NagService.ps1 @@ -0,0 +1,28 @@ +function Test-NagService { +<# +.SYNOPSIS + Tests an array of machines looking for a single instance of the Alkami Nag Service +.PARAMETER machineNames + An array of machines to test against. These machines should be a single pod grouping. +#> + param( + [array]$MachineNames + ) + $services = Get-Service -ComputerName $MachineNames -Name 'Alkami Nag Service' | Select-Object MachineName, Status + + [array]$runningServices = $services | Where-Object { $_.Status -eq "Running"} + + $runningServices | ForEach-Object { Write-Host $_.MachineName "is running nag" } + + if ($runningServices.Count -gt 1) { + + throw "There was more than one Nag Service running" + } + + if ($runningServices.Count -eq 0) { + throw "There are no servers running nag!" + } + + Write-Host "Nag is running successfully" +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-NagTriggers.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-NagTriggers.ps1 new file mode 100644 index 0000000..ea5c841 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-NagTriggers.ps1 @@ -0,0 +1,42 @@ +function Test-NagTriggers { +<# +.SYNOPSIS + Tests that the dbo.nag_triggers db contains valid triggers for the masterdatabase configuring the machine.config master connection string for a pod +#> + param( + + ) + $masterConnectionString = Get-MasterConnectionString + + try { + $conn = New-Object System.Data.SqlClient.SqlConnection + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + $conn.ConnectionString = $conStrBuilder.ToString() + + $conn.Open() + + $query = New-Object System.Data.SqlClient.SqlCommand("SELECT count(*) FROM [dbo].[NAG_TRIGGERS] where TRIGGER_STATE = 'WAITING'", $conn) + + $numberofWaitingTriggers = $query.ExecuteScalar() + + if ($numberofWaitingTriggers -eq 0 ) { + throw "No Triggers present for Nag, Check if Nag is running" + } + + Write-Host "Found $numberofWaitingTriggers Triggers in the waiting state." + } + catch { + Write-Warning "An exception occurred while trying to Check Nag Triggers" + Write-Warning $error[0] | Format-List -Force + return $null + } + finally { + if ($conn.State -ne [System.Data.ConnectionState]::Closed) { + $conn.Close() + } + + $conn = $null + } +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-RadiumMetaData.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-RadiumMetaData.ps1 new file mode 100644 index 0000000..47723f1 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-RadiumMetaData.ps1 @@ -0,0 +1,66 @@ +function Test-RadiumMetaData { +<# +.SYNOPSIS + Tests that dbo.JobMetaData contains some work, and that the number of failed jobs is less than 5% of the total number of jobs +#> + param( + + ) + $masterConnectionString = Get-MasterConnectionString + + try { + $conn = New-Object System.Data.SqlClient.SqlConnection + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString) + $conn.ConnectionString = $conStrBuilder.ToString() + + $conn.Open() + + $command = New-Object System.Data.SqlClient.SqlCommand("select Status, Count(status) as count from dbo.JobMetadata where RunDateTimeUtc > DateADD(mi, -30, GETUTCDATE()) group by status", $conn) + + $adapter = New-Object System.Data.sqlclient.sqlDataAdapter $command + + $dataset = New-Object System.Data.DataSet + $adapter.Fill($dataSet) | Out-Null + + $failedJobs = 0 + $totalJobs = 0 + $successfullJobs = 0 + + $dataset.Tables[0].Rows | ForEach-Object { + if ($_.Status -eq 4) { + $failedJobs += $_.Count + } + + if ($_.Status -eq 2) { + $successfullJobs += $_.Count + } + + $totalJobs += $_.Count + } + + if (($successfullJobs -eq 0) -and ($totalJobs -gt 0)) { + throw "No jobs have been successful in last 10 minutes, even though $totalJobs have ran" + } + + if (($totalJobs -gt 0) -and (($failedJobs / $totalJobs) -gt .005)) { + throw "the number of failed jobs exceeded 5% $failedJobs occurred out of $totalJobs" + } + + Write-Host "There are $totalJobs total jobs $failedJobs failed jobs, and $successfullJobs successful jobs in the last 10 minutes " + } + catch { + $warning = "An exception occurred while trying to Check Radium Triggers" + Write-Warning $warning + Write-Warning $error[0] | Format-List -Force + throw $warning + } + finally { + if ($conn.State -ne [System.Data.ConnectionState]::Closed) { + $conn.Close() + } + + $conn = $null + } +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Test-RadiumService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Test-RadiumService.ps1 new file mode 100644 index 0000000..ee33c83 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Test-RadiumService.ps1 @@ -0,0 +1,23 @@ +function Test-RadiumService { +<# +.SYNOPSIS + Tests an array of machines looking for instances of the radium service + +.PARAMETER MachineNames + An array of machines to test against. These machines should be a single pod grouping. +#> + param ( + [array]$MachineNames + ) + $services = Get-Service -ComputerName $MachineNames -Name 'Alkami Radium Scheduler Service' | Select-Object MachineName, Status + + [array]$runningServices = $services | Where-Object { $_.Status -eq "Running"} + + $runningServices | ForEach-Object { Write-Host $_.MachineName "is running radium" } + + if ($runningServices.Count -eq 0) { + throw "There are no servers running radium!" + } + Write-Host "Radium is running successfully" +} + diff --git a/Modules/Alkami.PowerShell.Services/Public/Uninstall-AlkamiService.ps1 b/Modules/Alkami.PowerShell.Services/Public/Uninstall-AlkamiService.ps1 new file mode 100644 index 0000000..23545d9 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Uninstall-AlkamiService.ps1 @@ -0,0 +1,108 @@ +function Uninstall-AlkamiService { +<# +.SYNOPSIS + Uninstalls an Alkami Windows Service (not a legacy microservice) + +.DESCRIPTION + Uninstalls an Alkami Windows Service (not a legacy microservice) + Legacy Microservices are considered to be Microservices that implement Alkami.Services.Subscriptions.ParticipatingService.DistributedServiceBase or derivatives. + This installer does not remove k8s or Lambdas or anything that is not a Windows Service. + This installer is intended to be used in conjunction with new practices from the Vanguard team + +.PARAMETER ServicePath + [string] The path to the service folder or service file. If to a folder, assumes that the file matches the path name. + Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path + +.EXAMPLE +Uninstall-AlkamiService C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [Alias("Path")] + [string]$ServicePath + ) + + $logLead = (Get-LogLeadName) + + $ServicePath = (Resolve-Path $ServicePath) + + if (!(Test-Path $ServicePath)) { + Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not uninstall something which does not exist." + return + } + + $parentPath = Split-Path -Path $ServicePath -Parent + + $serviceName = "" + if (-not (Get-Item -Path $servicePath).PSIsContainer) { + $serviceName = Split-Path -Path $ServicePath -Leaf + if ($serviceName.Substring($serviceName.Length - 4) -eq '.exe') { + $serviceName = $serviceName.Substring(0, $serviceName.Length - 4) + } + } + + $serviceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $ServicePath -ForceExact) + + # If we didn't find anything by the AssemblyInfo, let's double check the parent path + if (Test-IsCollectionNullOrEmpty $serviceCandidates) { + if (Test-StringIsNullOrWhiteSpace -Value $serviceName) { + $serviceCandidates = @(Get-ServiceInfoByCIMFragment -Fragment $parentPath) + if ($serviceCandidates.Count -gt 3) { + # 3 chosen because you could end up with 2 services in one folder, and that's not-great, but at 3 we clearly have a bad search + # This comment is for Cole to know that we just flat out couldn't get there from here + Write-Host "$logLead : Bad path given for trying to determine the root of the service to remove it" + } + if (!(Test-IsCollectionNullOrEmpty $serviceCandidates)) { + # This comment is for Cole to know that there were only 2, or more than 2, based on the prior output + # We will write out more details at the end + Write-Warning "$logLead : Could not find services under [$ServicePath] but found some under [$parentPath]" + } + } else { + $serviceCandidates = @(Get-ServiceInfoByCIMFragment -Fragment $serviceName -ForceExact) + } + } + + if (Test-IsCollectionNullOrEmpty $serviceCandidates) { + Write-Host "$logLead : No services found registered under [$ServicePath] or [$parentPath] or with [$serviceName]" + return + } + + $exactPathMatch = @($serviceCandidates.Where({ + # Manifest based installers allow for three paths + (Join-Path -Path $_.ParentFolder -ChildPath '\') -eq (Join-Path -Path $ServicePath -ChildPath 'lib\') -or + (Join-Path -Path $_.ParentFolder -ChildPath '\') -eq (Join-Path -Path $ServicePath -ChildPath 'app\') -or + (Join-Path -Path $_.ParentFolder -ChildPath '\') -eq (Join-Path -Path $ServicePath -ChildPath 'tools\') -or + (Join-Path -Path $_.ParentFolder -ChildPath '\') -eq (Join-Path -Path $ServicePath -ChildPath '\') + })) + + if ($exactPathMatch.Count -eq 1) { + $serviceCandidateName = $exactPathMatch.Name + $serviceCandidatePath = $exactPathMatch.Path + Write-Host "$logLead : Uninstalling existing service [$serviceCandidateName] at [$serviceCandidatePath] for [$ServicePath]" + + # Ensure it isn't running first + Stop-AlkamiService $serviceCandidateName + + Write-Verbose "$logLead : Removing [$serviceCandidateName] via sc.exe" + Invoke-SCExe @("delete", $serviceCandidateName) + + # Give Windows time to breathe. This might could be shorter, who knows. 150 is a magic number from thin air. + Start-Sleep -Milliseconds 150 + + if ($null -ne (Get-Service -Name $serviceCandidateName -ErrorAction Ignore)) { + # How did this happen? sc.exe is a hot-knife. + throw "$logLead : Tried to uninstall [$serviceCandidateName] from [$serviceCandidatePath] but it seems to still be present" + } + } else { + # If it couldn't do the thing, Cole needs to investigate, because the assumptions are bad + foreach($candidate in $serviceCandidates) { + Write-Warning "$logLead : TOO MANY SERVICES FOUND FOR [$ServicePath] : Found service [$($candidate.Name)] at [$($candidate.Path)]" + } + Write-Warning "$logLead : ---------------------" + Write-Warning "$logLead : You probably have an inconsistent package state. Please reinstall this service, then re-uninstall it." + Write-Warning "$logLead : It is worth slacking/paging out to Cole on this to see why we ended up in this weird state, because this was unexpected" + Write-Warning "$logLead : ---------------------" + throw "$logLead : More than 1 service candidates found in this location. Are you sure you wanna do this? Did you mean to specify a deeper path?" + } +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Public/Uninstall-LegacyMicroservice.ps1 b/Modules/Alkami.PowerShell.Services/Public/Uninstall-LegacyMicroservice.ps1 new file mode 100644 index 0000000..be19117 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Public/Uninstall-LegacyMicroservice.ps1 @@ -0,0 +1,142 @@ +function Uninstall-LegacyMicroservice { +<# +.SYNOPSIS + Uninstalls a legacy Alkami Windows Microservice + +.DESCRIPTION + Uninstalls a legacy Alkami Windows Microservice. + Legacy Microservices are considered to be Microservices that implement Alkami.Services.Subscriptions.ParticipatingService.DistributedServiceBase or derivatives. + Legacy Microservices uninstalled via this function are not uninstalled from a Service Fabric or other mesh implementation. + This installer does not remove k8s or Lambdas or anything that is not a Windows Service. + +.PARAMETER ServicePath + [string] The path to the service folder or service file. If to a folder, assumes that the file matches the path name. + Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path + +.PARAMETER AssemblyInfo + [string] The expected name of the service. This overrides the default option of matching the path ID. + +.PARAMETER UseLegacyConfigForServiceName + [switch] Look up the legacy service name from the config.ps1 file. This is non-preferred. This is only used if the AssemblyInfo parameter is empty. + +.EXAMPLE +Uninstall-LegacyMicroservice C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 0)] + [Alias("Path")] + [string]$ServicePath, + [Parameter(Mandatory = $false, Position = 1)] + [Alias("Name")] + [string]$AssemblyInfo, + [switch]$UseLegacyConfigForServiceName + ) + + $logLead = (Get-LogLeadName) + +<# +# This is gated behind the pipeline, so we have no need to test right now. +# If we do want to go this route, we definitely have to have a flag passed in. + if (!(Test-IsDeveloperMachine) -and (Test-IsWebServer)) { + Write-Warning "$loglead : Can not install microservices on the web tier" + return + } +#> + + $ServicePath = (Resolve-Path $ServicePath) + $folderPath = $ServicePath + + if (!(Test-Path $ServicePath)) { + Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not install something which does not exist." + return + } + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + if ([string]::IsNullOrWhiteSpace($AssemblyInfo) -and $UseLegacyConfigForServiceName) { + Write-Verbose "$logLead : Trying to find the `$AssemblyInfo from the legacy config.ps1" + $configPs1s = (Get-ChildItem -Path $ServicePath -Filter "config.ps1" -Recurse) + + $serviceId = "" + + foreach($config in $configPs1s) { + $configFullName = $config.FullName + + # Dotsource the file to get the contents into our runspace + . $configFullName + + if (![string]::IsNullOrWhiteSpace($serviceId)) { + # Write out where we got it from in case we need to debug why we got this from a wrong location, etc + Write-Verbose "$logLead : Found [$serviceId] in [$configFullName]]" + break + } + } + + # Store the value we got back so we can use it in other places + $AssemblyInfo = $serviceId + } + + $item = (Get-Item $ServicePath) + if ($item.PSIsContainer) { + # path was a folder + $folderName = (Split-Path -Path $ServicePath -Leaf) + if ($folderName -match 'tools') { + $ServicePath = (Split-Path -Path $ServicePath -Parent) + $folderName = (Split-Path -Path $ServicePath -Leaf) + } + $exeName = "$folderName.exe" + + if (![string]::IsNullOrWhiteSpace($AssemblyInfo)){ + $exeName = "$AssemblyInfo.exe" + } + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = $folderName + } + + Write-Host "$logLead : looking for $exeName in $ServicePath" + + $exeItem = (Get-ChildItem -Path $ServicePath -Filter $exeName -Recurse) | Select-Object -First 1 # there should only be one exe with the [package name].exe inside the folders + if ($null -eq $exeItem) { + $stopWatch.Stop() + Write-Warning "$logLead : Path passed in does not contain an exe with the filename: [$exeName]. Can not uninstall a non-existent microservice in [$($stopWatch.Elapsed)]." + return + } + $ServicePath = $exeItem.FullName + } else { + # The $item was not a .PSIsContainer so it was a file + # That means we want the folderPath to be the folder of the file + $folderPath = (Split-Path $folderPath -Parent) + + # We still don't have this value, let's pretend it's the folder name then + if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) { + $AssemblyInfo = (Split-Path -Path $ServicePath -Leaf).Replace(".exe","") + } + } + + # Check to see if the service is already registered before we try and do extra stuff + # This is where we could unregister it before we continue if we want to force a re-registration + $serviceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $AssemblyInfo) + + # If we didn't find anything by the assemblyinfo, let's double check the path in case something was found over there + if (Test-IsCollectionNullOrEmpty $serviceCandidates) { + $serviceCandidates = (Get-ServiceInfoByCIMFragment -Fragment $folderPath) + } + + # If we found something, log it and quit + if (Test-IsCollectionNullOrEmpty $serviceCandidates) { + $stopWatch.Stop() + Write-Warning "$logLead : No services were found, nothing to do, returning in [$($stopWatch.Elapsed)]" + return + } + + foreach($serviceCandidate in $serviceCandidates) { + Invoke-DeleteLegacyMicroserviceFromServiceCandidate $serviceCandidate + } + + $stopWatch.Stop() + + Write-Host "$logLead : [$assemblyinfo] uninstalled at [$ServicePath] in [$($stopWatch.Elapsed)]" +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/Resources/TriggerQuery.sql b/Modules/Alkami.PowerShell.Services/Resources/TriggerQuery.sql new file mode 100644 index 0000000..1463fd4 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/Resources/TriggerQuery.sql @@ -0,0 +1,5 @@ +SELECT ISNULL(t.NEXT_FIRE_TIME, '0') [FireTime], ISNULL(ft.SCHED_TIME, '0') [ScheduledTime], ISNULL(t.JOB_NAME, ft.JOB_NAME) [JobName] +FROM [dbo].[NAG_TRIGGERS] t (nolock) + LEFT JOIN [dbo].[NAG_FIRED_TRIGGERS] ft on ft.TRIGGER_NAME = t.TRIGGER_NAME +WHERE (t.TRIGGER_NAME like '%ScheduledTransfers%' or t.TRIGGER_NAME like '%AccountBatchFileImportJob%' or t.TRIGGER_NAME like '%CenlarBatchFileImport%') + AND (ft.SCHED_TIME IS NOT NULL OR (t.NEXT_FIRE_TIME > @now AND t.NEXT_FIRE_TIME < @threshold)) \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/tools/chocolateyInstall.ps1 b/Modules/Alkami.PowerShell.Services/tools/chocolateyInstall.ps1 new file mode 100644 index 0000000..b01306e --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/tools/chocolateyInstall.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Installing the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModulePath) { + ## If the target folder already existed, remove it, because we are re-installing this package, obviously + if (Test-Path $targetModuleVersionPath) { + Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } + + ## Clear previous children for name conflicts + (Get-ChildItem $targetModulePath) | ForEach-Object { + Write-Information "Removing module located at [$_]"; + Remove-Item $_.FullName -Recurse -Force; + } + } + + Write-Host "Copying module $id to [$targetModuleVersionPath]"; + Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force; + +} \ No newline at end of file diff --git a/Modules/Alkami.PowerShell.Services/tools/chocolateyUninstall.ps1 b/Modules/Alkami.PowerShell.Services/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..7c36766 --- /dev/null +++ b/Modules/Alkami.PowerShell.Services/tools/chocolateyUninstall.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +Param() +process { + $myCurrentPath = $PSScriptRoot; + Write-Verbose "Uninstalling the Module from $myCurrentPath"; + + $parentPath = (Split-Path $myCurrentPath); + $systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\"; + $myModulePath = (Join-Path $parentPath "module"); + + $metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata; + + $id = $metadata.id; + + $version = $metadata.version -replace '-pre.+',''; + + $targetModulePath = (Join-Path $systemModulePath $id); + $targetModuleVersionPath = (Join-Path $targetModulePath $version); + + if (Test-Path $targetModuleVersionPath) { + Write-Information "Removing module at [$targetModuleVersionPath]!!" + Remove-Item $targetModuleVersionPath -Recurse -Force; + } +} + diff --git a/Modules/Cole.PowerShell.Developer/AlkamiManifest.xml b/Modules/Cole.PowerShell.Developer/AlkamiManifest.xml new file mode 100644 index 0000000..c27d88c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/AlkamiManifest.xml @@ -0,0 +1,12 @@ + + + 1.0 + + Alkami + Cole.PowerShell.Developer + SREModule + + + Production + + diff --git a/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.nuspec b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.nuspec new file mode 100644 index 0000000..69e502b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.nuspec @@ -0,0 +1,26 @@ + + + + Cole.PowerShell.Developer + 1.0.5 + Alkami Platform Modules - PowerShell - Developer - More Cole Magic + Alkami Technologies + Alkami Technologies + https://confluence.alkami.com/display/~cbrand/Cole%27s+PowerShell+Module + https://confluence.alkami.com/download/attachments/655424/user-avatar + https://confluence.alkami.com/display/~cbrand/Cole%27s+PowerShell+Module + false + Installs Cole's Alkami Developer module for use with PowerShell. + + PowerShell + Copyright (c) 2020 Alkami Technologies + + + + + + + + + + diff --git a/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.ps1xml b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.ps1xml new file mode 100644 index 0000000..b37ae28 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.ps1xml @@ -0,0 +1 @@ +DriveInfoTypesDriveInfoScrumTeamTypesScrumTeamAWSTypesAWSConfigEntryAWSCredentialEntryAWSCredentialEntryHostsFileEntryTypesHostsFileEntryInstanceTypesInstanceServiceFailureActionsServiceFailureActionServiceObjectsServiceObjectJiraTeamTypesJiraTeamJiraTeamMemberTypesJiraTeamMemberDefaultDriveInfoViewDriveInfoTypesrightrightDeviceIDFileSystemVolumeNameFreeSpaceGBSizeGBDefaultScrumTeamViewScrumTeamTypesTeamProductOwnerStrategicProductManagerScrumMasterTechLeadEngManagerTeamSlackChannelDefaultAWSConfigEntryViewAWSTypesNamerole_arnmfa_serialregionsource_profileDefaultAWSCredentialEntryViewAWSCredentialEntryProfileaws_access_key_idtoolkit_artifact_guidregionoutputDefaultHostsFileEntryViewHostsFileEntryTypesIpAddressHostnameIsDisabledCommentDefaultInstanceViewInstanceTypesInstanceIdHostnamePrivateIpAddressDesignationInstanceTypeCaptureStateRegionDefaultServiceFailureActionViewServiceFailureActionsResetPeriodActionTypesCommandRebootMessageDefaultServiceObjectViewServiceObjectsNameRunAsStartModeExePathDefaultJiraTeamViewJiraTeamTypesNameDepartmentLead$_.Manager.Name -join ','$_.ScrumMaster.Name -join ','$_.Members.Name -join ','DefaultJiraTeamMemberViewJiraTeamMemberTypesNameRoleDisplayNameInactive diff --git a/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.psd1 b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.psd1 new file mode 100644 index 0000000..e1c2b02 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.psd1 @@ -0,0 +1,13 @@ +@{ + RootModule = 'Cole.PowerShell.Developer.psm1' + ModuleVersion = '1.0.6' + GUID = 'ec8c8e72-c789-4d20-bcde-1633c39a1918' + Author = 'cbrand' + CompanyName = 'Alkami Technologies, Inc.' + Copyright = '(c) 2020 Alkami Technologies, Inc. All rights reserved.' + PowerShellVersion = '5.0' + RequiredModules = 'Alkami.PowerShell.Common' + FunctionsToExport = 'Add-HostsFileEntry','Add-JiraComment','Add-OrbHostEntries','Assert-ValidAWSProfileName','Checkpoint-AllAvailableRepos','Checkpoint-EC2Instances','Clear-AlkamiModules','Clear-AWSSQSQueue','Clear-LocalUserCredentials','Clear-OldSqlLogs','Close-TeamCityBlock','Compare-File','Compare-Folders','ConvertFrom-Base64','ConvertFrom-EC2Instance','ConvertFrom-FailureActions','ConvertFrom-SlnFile','ConvertTo-ASCII','ConvertTo-AWSConfigEntry','ConvertTo-AWSCredentialEntry','ConvertTo-Base64','ConvertTo-DriveInfo','ConvertTo-Hash','ConvertTo-Hashtable','ConvertTo-HostsFileEntry','ConvertTo-Instance','ConvertTo-JiraTeam','ConvertTo-JiraTeamMember','ConvertTo-ScrumTeam','Convert-TypeForHelpDisplay','Disable-HostsFileEntry','Enable-HostsFileEntry','Expand-Path','Find-AssembliesThatExportType','Find-AssembliesThatReferenceByFragment','Find-Command','Find-GitExecutablePath','Find-GitRepoRootFromPath','Find-MissingReferences','Format-BoldText','Format-HostsFileRecord','Format-TextJustified','Format-TextWrapToDisplay','Format-UnderlineText','Format-XML','Get-AllAppSettings','Get-AllInstalledComponentsByType','Get-AllRunningServicesForEnvironment','Get-AllServices','Get-AllUsersNotLoggedInSince','Get-AWSAccessKey','Get-AWSConfigEntries','Get-AWSCredentialEntries','Get-AWSCredentialSetter','Get-AWSRegions','Get-AwsStandardDynamicParameters','Get-BasicAuthWebHeader','Get-BitlockerDriveInformation','Get-CachedInstances','Get-CacheFile','Get-CachePathDesignations','Get-CachePathEC2Instance','Get-CachePathJiraTeams','Get-Callstack','Get-CallstackParentFunctionNames','Get-CallstackWrapper','Get-ChatFunkyString','Get-Coalesce','Get-ComponentInstallerInstallPath','Get-ComponentInstallerUninstallPath','Get-ConfluenceBaseUrl','Get-ConsoleDisplayWidth','Get-CredentialFromEnvironmentVariables','Get-CurrentDomainName','Get-CurrentUsername','Get-Designations','Get-DotNetProjectFileTypeFromGuid','Get-DynamicAwsProfilesParameter','Get-DynamicAwsRegionParameter','Get-FunctionWriteProgressHelperCalls','Get-GitBranchNames','Get-GitCommandAddDefinition','Get-GitCommandApplyDefinition','Get-GrandParentFunctionName','Get-HackerText','Get-HistoryEntries','Get-HostsFileAllRecords','Get-HostsFilePath','Get-IFConfig','Get-JiraBaseUrl','Get-JiraBearerTokenAuthWebHeader','Get-JiraTeams','Get-JiraTicketMetaFields','Get-Json','Get-JsonStringLeadsByDepth','Get-KnownDeveloperHostsEntries','Get-LastWebRequestErrorText','Get-LocalCachedAWSProfile','Get-LocalConfiguredAWSProfileNames','Get-LocalHardDriveRoots','Get-LocalHardDrives','Get-MostUsedCommand','Get-NormalizedPath','Get-ParentFunctionName','Get-ProgramDataPath','Get-RepoCheckpointPath','Get-ScrumTeams','Get-ScrumTeamsFromConfluence','Get-SemverHistory','Get-ServersByPod','Get-SiteTempDirectoryPath','Get-SSHConfigEntries','Get-TenableAssetTag','Get-UpgradePackagesBetweenEnvironments','Get-Uptime','Get-UrlComponents','Get-WindowsThreadSliceTime','Grant-AclOnCert','Group-Numbers','Import-AlkamiConsoleHost','Import-FigletFontFile','Initialize-AWSCredentials','Install-AlkamiDeveloperSQLServer','Install-AlkamiPackage','Invoke-Build','Invoke-CallOperatorWithPathAndParameters','Invoke-CategorizeAllProjects','Invoke-CategorizeCSProj','Invoke-Configure','Invoke-Database','Invoke-Deploy','Invoke-Hosts','Invoke-Install','Invoke-InstallerWidget','Invoke-JobRunner','Invoke-Notes','Invoke-Package','Invoke-QueryByConnectionString','Invoke-ScriptedActions','Invoke-Shutdown','Invoke-Test','Join-UrlComponents','Merge-Objects','New-AWSConfigFile','New-AWSCredentialsFile','New-Directory','New-File','New-JiraDevTicket','New-JiraTicket','New-List','Open-TeamCityBlock','Optimize-CIPackageInstallList','Publish-TeamCityArtifact','Read-GitConfig','Read-StreamAsString','Read-Xml','Remove-AllInvalidComponentInstallerPath','Remove-DuplicateHostsFileRecords','Remove-HostsFileEntry','Remove-OrbHostEntries','Remove-TextStyle','Reset-TerminalColor','Restore-AllProviders','Restore-AllWebApplications','Restore-AllWebExtensions','Restore-AllWidgets','Save-CompleteHostsFile','Search-History','Select-RightSubstringWithPadLeft','Set-JiraBearerToken','Set-LocalCachedAWSProfile','Set-LocalUserCredential','Set-PathVariable','Set-RepoCheckpointPath','Set-TeamCityParameter','Set-WindowsDisplayPercentage','Show-Box','Show-CommandDefinition','Show-Help','Show-Line','Show-ListAsTable','Show-ToastNotification','Show-Verbs','Test-ArrayBinding','Test-CallProgetForPackages','Test-CallProgetForVersions','Test-CustomSuppressMessage','Test-DoubleParameterSets','Test-InvocationOfCommand','Test-IsArrayValid','Test-IsCurrentAWSUserSessionValid','Test-IsFalse','Test-IsGitFolderRoot','Test-IsInteractiveSession','Test-IsMacOSPlatform','Test-IsSourceFileVersionHigherThanTarget','Test-IsTrue','Test-IsUnixPlatform','Test-IsUserLocalAdministrator','Test-IsWindowsDesktop','Test-IsWindowsPlatform','Test-MyInvocationCommand','Test-SplatUsage','Test-TerminalSupportsANSIEscape','Test-WasFileModifiedWithin','Test-WriteProgressHelper','Test-WriteProgressHelperChild','Trace-ActionEnd','Trace-ActionStart','Trace-ClearActions','Update-AlkamiModules','Update-AWSAccessKey','Update-AWSCLIAccessKey','Update-LastModifiedTimestamp','Update-PowerShellModuleVersion','Use-ProductionJira','Use-Staging2Jira','Use-StagingJira','Use-TerraformVersion','Write-ErrorObject','Write-ExceptionToStringBuilder','Write-JsonArrayNameValuePair','Write-JsonNamePrimitiveArrayPair','Write-JsonNamePrimitiveValuePair','Write-JsonNameStringValuePair','Write-JsonObject','Write-OrderedJson','Write-OrderedJsonByArray','Write-ProgressHelper','Write-TeamCity','Write-TeamCityBuildProblem' + FormatsToProcess = "Cole.PowerShell.Developer.ps1xml" + AliasesToExport = 'Any','build','build','Combine','configure','db','db','deploy','deploy','hosts','hosts','install','install','IsFalse','IsTrue','Mixin','New-Folder','New-HostsFileEntry','New-JiraIssue','package','Reset-Color','Reset-PowerShellColor','test','test','Test-IsAwsSessionValid','touch','Update-HostsFileEntry','uptime','which' +} diff --git a/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.pssproj b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.pssproj new file mode 100644 index 0000000..5cc649c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Cole.PowerShell.Developer.pssproj @@ -0,0 +1,40 @@ + + + Debug + 2.0 + {f076c5f2-1bf5-4fbb-b59f-26ab9f2a2bbb} + Exe + Cole.PowerShell.Developer + Cole.PowerShell.Developer + Cole.PowerShell.Developer + Invoke-Pester; + ..\build-project.ps1 (Join-Path $(SolutionDir) "Cole.PowerShell.Developer") + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJson.ps1 b/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJson.ps1 new file mode 100644 index 0000000..fe4c3ff --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJson.ps1 @@ -0,0 +1,36 @@ +function Get-FlattenedAppsettingJson { + param ( + [object]$Json + ) + + if ($null -eq $Json) { + Write-Warning "`$Json -eq `$null" + return $null + } + + $logLead = (Get-LogLeadName) + + $returnValues = @() + + if ($Json.GetType().Name -eq "PSCustomObject") { + $Json = ConvertTo-Hash $Json + } + + if ($Json.GetType().Name -match "Hashtable") { + foreach ($key in $Json.Keys) { + $returnValues += (Get-FlattenedAppsettingJsonKeyValue $key $Json[$key]) + } + } elseif ($Json -is [System.Collections.IEnumerable] -and $Json -isnot [string]) { + for($i = 0; $i -lt $Json.Count; $i++) { + $item = $Json[$i] + $deepPairs = (Get-FlattenedAppsettingJson $item) + foreach($pair in $deepPairs) { + $returnValues += @{ Key = "$Key`[$i]`:$($pair.Key)"; Value = $pair.Value} + } + } + } else { + # return @{ Value = $Json; } + } + + return $returnValues +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJsonKeyValue.ps1 b/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJsonKeyValue.ps1 new file mode 100644 index 0000000..d717f3c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Private/Get-FlattenedAppsettingJsonKeyValue.ps1 @@ -0,0 +1,31 @@ +function Get-FlattenedAppsettingJsonKeyValue { + param( + [string]$Key, + [object]$Value + ) + + $returnValues = @() + + if ($Value.GetType().Name -eq "PSCustomObject") { + $Value = ConvertTo-Hash $Value + } + + if ($Value.GetType().Name -match "Hashtable") { + $deepPairs = (Get-FlattenedAppsettingJson $Value) + foreach($pair in $deepPairs) { + $returnValues += @{ Key = "$Key`:$($pair.Key)"; Value = $pair.Value} + } + } elseif ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { + for($i = 0; $i -lt $Value.Count; $i++) { + $item = $Value[$i] + $deepPairs = (Get-FlattenedAppsettingJson $item) + foreach($pair in $deepPairs) { + $returnValues += @{ Key = "$Key`[$i]`:$($pair.Key)"; Value = $pair.Value} + } + } + } else { + $returnValues += @{ Key = $Key; Value = $Value; } + } + + return $returnValues +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Private/global_declarations.ps1 b/Modules/Cole.PowerShell.Developer/Private/global_declarations.ps1 new file mode 100644 index 0000000..e8093f0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Private/global_declarations.ps1 @@ -0,0 +1,310 @@ +if ($null -eq $PSStyle) { + <# + Shim in PSStyle if it isn't already supported + This does not do anything to change that Microsoft does not adhere to the https://no-color.org/ premise + Any functions we write that take advantage of $PSStyle should be aware of the NO_COLOR variable presence + see also for more inspiration https://duffney.io/usingansiescapesequencespowershell/ + #> + $esc = [char]27 + $global:PSStyle = New-Object PSCustomObject -Property @{ + ForegroundColor = New-Object PSCustomObject -Property @{ + Black = "$esc[30m"; + Blue = "$esc[34m"; + Cyan = "$esc[36m"; + DarkGray = "$esc[90m"; + Green = "$esc[32m"; + LightBlue = "$esc[94m"; + LightCyan = "$esc[96m"; + LightGray = "$esc[97m"; + LightGreen = "$esc[92m"; + LightMagenta = "$esc[95m"; + LightRed = "$esc[91m"; + LightYellow = "$esc[93m"; + Magenta = "$esc[35m"; + Red = "$esc[31m"; + White = "$esc[37m"; + Yellow = "$esc[33m"; + } + BackgroundColor = New-Object PSCustomObject -Property @{ + Black = "$esc[40m"; + Blue = "$esc[44m"; + Cyan = "$esc[46m"; + DarkGray = "$esc[100m"; + Green = "$esc[42m"; + LightBlue = "$esc[104m"; + LightCyan = "$esc[106m"; + LightGray = "$esc[107m"; + LightGreen = "$esc[102m"; + LightMagenta = "$esc[105m"; + LightRed = "$esc[101m"; + LightYellow = "$esc[103m"; + Magenta = "$esc[45m"; + Red = "$esc[41m"; + White = "$esc[47m"; + Yellow = "$esc[43m"; + } + FormattingData = New-Object PSCustomObject -Property @{ + FormatAccent = "$esc[32;1m"; + ErrorAccent = "$esc[36;1m"; + Error = "$esc[31;1m"; + Warning = "$esc[33;1m"; + Verbose = "$esc[33;1m"; + Debug = "$esc[33;1m"; + } + Reset = "$esc[0m"; + BlinkOff = "$esc[5m"; + Blink = "$esc[25m"; + BoldOff = "$esc[22m"; + Bold = "$esc[1m"; + Hidden = "$esc[8m"; + HiddenOff = "$esc[28m"; + Reverse = "$esc[7m"; + ReverseOff = "$esc[27m"; + ItalicOff = "$esc[23m"; + Italic = "$esc[3m"; + UnderlineOff = "$esc[24m"; + Underline = "$esc[4m"; + } +} + +if ($null -eq $PSBox) { + $global:PSBox = New-Object PSCustomObject -Property @{ + Horizontal = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2500 + + Dashed = [string][char]0x2504 + DashedNarrow = [string][char]0x2508 + DashedWide = [string][char]0x254c + + LeftHalf = [string][char]0x2574 + RightHalf = [string][char]0x2576 + + Bold = [string][char]0x2501 + BoldDashed = [string][char]0x2505 + BoldDashedNarrow = [string][char]0x2509 + BoldDashedWide = [string][char]0x254d + BoldLeftHalf = [string][char]0x2578 + BoldRightHalf = [string][char]0x257a + BoldRightThinLeft = [string][char]0x257c + BoldLeftThinRight = [string][char]0x257e + + Double = [string][char]0x2550 + } + Vertical = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2502 + + Dashed = [string][char]0x2506 + DashedNarrow = [string][char]0x250a + DashedWide = [string][char]0x254e + + HalfTop = [string][char]0x2575 + HalfBottom = [string][char]0x2577 + + Bold = [string][char]0x2503 + BoldDashed = [string][char]0x2507 + BoldDashedNarrow = [string][char]0x250b + BoldDashedWide = [string][char]0x254f + BoldTopHalf = [string][char]0x2579 + BoldTopThinBottom = [string][char]0x257f + BoldBottomHalf = [string][char]0x257b + BoldBottomThinTop = [string][char]0x257d + + Double = [string][char]0x2551 + } + Corner = New-Object PSCustomObject -Property @{ + TopLeft = New-Object PSCustomObject -Property @{ + Line = [string][char]0x250c + + Bold = [string][char]0x250f + BoldHorizontal = [string][char]0x250d + BoldVertical = [string][char]0x250e + + Round = [string][char]0x256d + + Double = [string][char]0x2554 + DoubleHorizontal = [string][char]0x2552 + DoubleVertical = [string][char]0x2553 + } + TopRight = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2510 + + Bold = [string][char]0x2513 + BoldHorizontal = [string][char]0x2511 + BoldVertical = [string][char]0x2512 + + Round = [string][char]0x256e + + Double = [string][char]0x2557 + DoubleHorizontal = [string][char]0x2555 + DoubleVertical = [string][char]0x2556 + } + BottomLeft = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2514 + + Bold = [string][char]0x2517 + BoldHorizontal = [string][char]0x2515 + BoldVertical = [string][char]0x2516 + + Round = [string][char]0x2570 + + Double = [string][char]0x255a + DoubleHorizontal = [string][char]0x2558 + DoubleVertical = [string][char]0x2559 + } + BottomRight = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2518 + + Bold = [string][char]0x251b + BoldHorizontal = [string][char]0x2519 + BoldVertical = [string][char]0x251a + + Round = [string][char]0x256f + + Double = [string][char]0x255d + DoubleHorizontal = [string][char]0x255b + DoubleVertical = [string][char]0x255c + } + } + Tee = New-Object PSCustomObject -Property @{ + Vertical = New-Object PSCustomObject -Property @{ + Left = New-Object PSCustomObject -Property @{ + Line = [string][char]0x251c + + Bold = [string][char]0x2523 + BoldHorizontal = [string][char]0x251d + BoldTop = [string][char]0x251e + BoldBottom = [string][char]0x251f + BoldVertical = [string][char]0x2520 + + ThinBottom = [string][char]0x2521 + ThinTop = [string][char]0x2522 + + Double = [string][char]0x2560 + DoubleHorizontal = [string][char]0x255e + DoubleVertical = [string][char]0x255f + } + Right = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2524 + + Bold = [string][char]0x252b + BoldHorizontal = [string][char]0x2525 + BoldTop = [string][char]0x2526 + BoldBottom = [string][char]0x2527 + BoldVertical = [string][char]0x2528 + + ThinBottom = [string][char]0x2529 + ThinTop = [string][char]0x252a + + Double = [string][char]0x2563 + DoubleHorizontal = [string][char]0x2561 + DoubleVertical = [string][char]0x2562 + } + } + Horizontal = New-Object PSCustomObject -Property @{ + Top = New-Object PSCustomObject -Property @{ + Line = [string][char]0x252c + + Bold = [string][char]0x2533 + BoldLeft = [string][char]0x252d + BoldRight = [string][char]0x252e + BoldHorizontal = [string][char]0x252f + BoldVertical = [string][char]0x2530 + + ThinRight = [string][char]0x2531 + ThinLeft = [string][char]0x2532 + + Double = [string][char]0x2566 + DoubleHorizontal = [string][char]0x2564 + DoubleVertical = [string][char]0x2565 + } + Bottom = New-Object PSCustomObject -Property @{ + Line = [string][char]0x2534 + + Bold = [string][char]0x253b + BoldLeft = [string][char]0x2535 + BoldRight = [string][char]0x2536 + BoldHorizontal = [string][char]0x2537 + BoldVertical = [string][char]0x2538 + + ThinRight = [string][char]0x2539 + ThinLeft = [string][char]0x253a + + Double = [string][char]0x2569 + DoubleHorizontal = [string][char]0x2567 + DoubleVertical = [string][char]0x2568 + } + } + Cross = New-Object PSCustomObject -Property @{ + Line = [string][char]0x253c + + Bold = [string][char]0x254b + BoldLeft = [string][char]0x253d + BoldRight = [string][char]0x253e + BoldHorizontal = [string][char]0x253f + BoldTop = [string][char]0x2540 + BoldBottom = [string][char]0x2541 + BoldVertical = [string][char]0x2542 + BoldLeftTop = [string][char]0x2543 + BoldRightTop = [string][char]0x2544 + BoldLeftBottom = [string][char]0x2545 + BoldRightBottom = [string][char]0x2546 + + ThinBottom = [string][char]0x2547 + ThinTop = [string][char]0x2548 + ThinRight = [string][char]0x2549 + ThinLeft = [string][char]0x254a + + Double = [string][char]0x256c + DoubleHorizontal = [string][char]0x256a + DoubleVertical = [string][char]0x256b + } + } + Diagonal = New-Object PSCustomObject -Property @{ + Line45 = [string][char]0x2571 + Line135 = [string][char]0x2572 + Cross = [string][char]0x2573 + } + Shading = New-Object PSCustomObject -Property @{ + Block = New-Object PSCustomObject -Property @{ + Full = [string][char]0x2588 + OneQuarter = [string][char]0x2591 + Half = [string][char]0x2592 + ThreeQuarter = [string][char]0x2593 + BottomLeft = [string][char]0x2596 + BottomRight = [string][char]0x2597 + TopLeft = [string][char]0x2598 + InvertedTopRight = [string][char]0x2599 + TopLeftBottomRight = [string][char]0x259a + InvertedBottomRight = [string][char]0x259b + InvertedBottomLeft = [string][char]0x259c + TopRight = [string][char]0x259d + TopRightBottomLeft = [string][char]0x259e + InvertedTopLeft = [string][char]0x259f + Horizontal = New-Object PSCustomObject -Property @{ + InvertedEighth = [string][char]0x2594 + InvertedHalf = [string][char]0x2580 + OneEighth = [string][char]0x2581 + OneQuarter = [string][char]0x2582 + ThreeEighth = [string][char]0x2583 + Half = [string][char]0x2584 + FiveEighth = [string][char]0x2585 + ThreeQuarter = [string][char]0x2586 + SevenEighth = [string][char]0x2587 + } + Vertical = New-Object PSCustomObject -Property @{ + SevenEighth = [string][char]0x2589 + ThreeQuarter = [string][char]0x258a + FiveEighth = [string][char]0x258b + Half = [string][char]0x258c + ThreeEighth = [string][char]0x258d + OneQuarter = [string][char]0x258e + OneEighth = [string][char]0x258f + InvertedHalf = [string][char]0x2590 + InvertedEighth = [string][char]0x2595 + } + } + } + } +} + +$global:TraceActionList = New-Object -TypeName "System.Collections.ArrayList" diff --git a/Modules/Cole.PowerShell.Developer/Public/Add-HostsFileEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/Add-HostsFileEntry.ps1 new file mode 100644 index 0000000..edef317 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Add-HostsFileEntry.ps1 @@ -0,0 +1,57 @@ +function Add-HostsFileEntry { +<# +.SYNOPSIS + Updates or adds a specific hosts file entry to the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [Parameter(Mandatory = $false)] + [string]$Comment + ) + + $logLead = Get-LogLeadName + + $records = @() + $existingRecords = Get-HostsFileAllRecords + + $foundPriorRecord = $false + foreach ($record in $existingRecords) { + if ($record.Hostname -eq $Hostname) { + $foundPriorRecord = $true + + $record.IpAddress = $IpAddress + # Only update the comment if we passed one in. Ignore the previous comment and overwrite (frequently this is for adding jira tickets on changes) + # We could alternately prepend this comment to the existing comment, with a comma+space separator + if (![string]::IsNullOrWhiteSpace($Comment)) { + $record.Comment = $Comment + } + + $formattedRecord = Format-HostsFileRecord -Record $record + + Write-Host "$logLead : Updating record for $formattedRecord" + } + $records += $record + } + + if (!$foundPriorRecord) { + $newRecord = New-HostsFileEntry -IpAddress $IpAddress -Hostname $Hostname -Comment $Comment + $formattedRecord = Format-HostsFileRecord -Record $record + + Write-Host "$logLead : Adding record for $formattedRecord" + + $records += $newRecord + } + + Save-CompleteHostsFile -Record $records +} + +Set-Alias -Name Update-HostsFileEntry -Value Add-HostsFileEntry \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Add-JiraComment.ps1 b/Modules/Cole.PowerShell.Developer/Public/Add-JiraComment.ps1 new file mode 100644 index 0000000..c6bca4a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Add-JiraComment.ps1 @@ -0,0 +1,91 @@ +function Add-JiraComment { +<# +.SYNOPSIS + Add the specified comment body to the specified Jira ticket + +.PARAMETER TicketNumber + The Jira ticket number + +.PARAMETER Message + The content of the message + +.PARAMETER Credential + Optional. Credentials to talk to Jira. Expected to be stored in a user-environment-variable for the default case. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string]$TicketNumber, + [Parameter(Mandatory = $true, Position = 1)] + [string]$Message, + [Parameter(Mandatory = $false, Position = 2)] + [PSCredential]$Credential + ) + + $logLead = (Get-LogLeadName) + + if ($null -eq $Credential) { + $Credential = (Get-CredentialFromEnvironmentVariables) + } + + if ($null -eq $Credential) { + Write-Error "$logLead : Can not talk to Jira without credentials. Returning." + return + } + + $headers = (Get-BasicAuthWebHeader -Credential $Credential) + $headers["Content-Type"] = "application/json" + + $url = (Get-JiraBaseUrl) + # Use this URL to ensure the ticket number as provided exists + $jiraUrlTicket = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue/$TicketNumber") + + $arguments = @{ + UseBasicParsing = $true + Headers = $headers + Uri = $jiraUrlTicket + Method = 'Get' + } + + try { + $response = Invoke-RestMethod @arguments + if (![string]::IsNullOrWhiteSpace($response.key)) { + # In case the ticket has been moved, let's goto the right value + $TicketNumber = $response.key + } + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + Write-Error "$logLead : Could not verify ticket. Ensure proper credentials and try again, or verify the ticket number is correct." + return + } + + # Use this URL to post the comment to the body + $jiraUrlComment = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue/$TicketNumber/comment") + + $body = @{ + body = $Message + } + + $arguments = @{ + UseBasicParsing = $true + Headers = $headers + Uri = $jiraUrlComment + Body = (ConvertTo-Json $body -Depth 10) + Method = 'Post' + } + + try { + $response = (Invoke-RestMethod @arguments) + if (![string]::IsNullOrWhiteSpace($response.key)) { + # In case the ticket has been moved, let's goto the right value + $TicketNumber = $response.key + } + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + Write-Error "$logLead : Could not add comment to ticket. Ensure proper credentials and try again, or verify the ticket number is correct." + return + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Add-OrbHostEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Add-OrbHostEntries.ps1 new file mode 100644 index 0000000..e8b275a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Add-OrbHostEntries.ps1 @@ -0,0 +1,21 @@ +function Add-OrbHostEntries { +<# +.SYNOPSIS + Adds all the known ORB entries to the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + [OutputType([string])] + param() + + $logLead = Get-LogLeadName + + $knownHostEntries = (Get-KnownDeveloperHostsEntries) + $existingRecords = Get-Host + foreach ($entry in $knownHostEntries) { + Write-Host "$logLead : Adding hosts file with IpAddress $($entry.IpAddress) and Hostname $($entry.Hostname)" + Add-HostsFileEntry -IpAddress $entry.IpAddress -Hostname $entry.Hostname + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Assert-ValidAWSProfileName.ps1 b/Modules/Cole.PowerShell.Developer/Public/Assert-ValidAWSProfileName.ps1 new file mode 100644 index 0000000..85270af --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Assert-ValidAWSProfileName.ps1 @@ -0,0 +1,45 @@ +function Assert-ValidAWSProfileName { +<# +.SYNOPSIS + Used to verify that the provided name matches one of the expected AWS Profile Names commonly in use +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ProfileName, + [switch]$ArgumentValidationScript + ) + + $logLead = (Get-LogLeadName) + + $ExpectedProfileNames = @( + 'temp-mp', + 'temp-prod', + 'temp-sandbox', + 'temp-dev', + 'temp-qa', + 'temp-corp', + 'temp-transit', + 'temp-transitnp', + 'temp-security', + 'MP', + 'Prod', + 'Sandbox', + 'Dev', + 'Qa', + 'Corp', + 'Transit', + 'TransitNP', + 'Security' + ) + + if ($ExpectedProfileNames -notcontains $ProfileName) { + throw "$logLead : Profile name [$ProfileName] doesn't match one of the expected options. Did you mean one of [$($ExpectedProfileNames -join ',')]?" + } + + if ($ArgumentValidationScript) { + return $true + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Checkpoint-AllAvailableRepos.ps1 b/Modules/Cole.PowerShell.Developer/Public/Checkpoint-AllAvailableRepos.ps1 new file mode 100644 index 0000000..c3230f3 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Checkpoint-AllAvailableRepos.ps1 @@ -0,0 +1,245 @@ +function Checkpoint-AllAvailableRepos { +<# +.SYNOPSIS + Backup all the available repositories you have access to from bitbucket to a known location + +.PARAMETER StartingFolder + [string] Where do you want to start storing the repos. + +.PARAMETER ApiKey + [string] A bitbucket API key + +.PARAMETER BranchName + [string] A target branch name. If not present will default to, in order, 'main', 'master', 'develop' + +.PARAMETER ReplacePath + [string] If you want to replace part of your checkout path from the clone url. This is mostly useful with ssh config files. + +.PARAMETER ReplacePrefix + [string] Used with ReplacePath, only really useful if you use ssh config files with heavy modification +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$Path = (Get-RepoCheckpointPath), + [Parameter(Mandatory = $false, Position = 1)] + [string]$ApiKey = "", + [Parameter(Mandatory = $false, Position = 2)] + [string]$BranchName, + [Parameter(Mandatory = $false, Position = 3)] + [string]$ReplacePath = '', + [Parameter(Mandatory = $false, Position = 4)] + [string]$ReplacePrefix = '', + # TODO: Project-only list instead of project-skip list + [Parameter(Mandatory = $false, Position = 5)] + [string[]]$ProjectSkipList = @(), + [Parameter(Mandatory = $false, Position = 6)] + [string[]]$RepositorySkipList = @('iosdev','dba','themes'), + [Parameter(Mandatory = $false, Position = 7)] + [object[]]$SpecificProjectRepoSkipList = @(@{Key='dsn';Slug='old-design-repo-old';},@{Key='dsn';Slug='internal';}) + ) + + $logLead = (Get-LogLeadName) + + # Fix some git nonsense + Set-EnvironmentVariable -Name "GIT_REDIRECT_STDERR" -Value '2>&1' -StoreName Machine + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "$logLead : No path found for checkpoint location!" + } + + $branchNameEmpty = [string]::IsNullOrWhiteSpace($BranchName) + $currentLocation = (Get-Location) + + if ([string]::IsNullOrWhiteSpace($ApiKey)) { + $ApiKey = (Get-EnvironmentVariable -Name "Checkpoint_AllAvailableRepos_ApiKey" 6>$null 5>$null 4>$null 3>$null) + } + + if (!(Test-Path $Path)) { + (New-Item -ItemType Directory -Path $Path -Force) | Out-Null + } + + Set-Location $Path + $removalFilePath = (Join-Path -Path $Path -ChildPath "RemoveFiles.txt") + $pathReferencePath = (Join-Path -Path $Path -ChildPath "PathReference.txt") + (New-Item -Path $removalFilePath -ItemType File -Force -ErrorAction Ignore) | Out-Null + (New-Item -Path $pathReferencePath -ItemType File -Force -ErrorAction Ignore) | Out-Null + + $replaceUrlParts = ![string]::IsNullOrWhiteSpace($ReplacePath) -and ![string]::IsNullOrWhiteSpace($ReplacePrefix) + $sshConfigEntries = @() + $hasSshConfigEntries = $false + + if (!$replaceUrlParts) { + try { + $sshConfigEntries = @(Get-SSHConfigEntries) + $hasSshConfigEntries = $null -ne $sshConfigEntries + } catch { + Write-Verbose "$logLead : Was not able to get a config entry, this might be a problem, but probably ok" + } + } + + $currentUserCredential = $null + try { + $currentUserCredential = Get-CredentialFromEnvironmentVariables + } catch {} + + if ([string]::IsNullOrWhiteSpace($ApiKey)) { + Write-Warning "$logLead : Can not continue without an API Key for Bitbucket." + Write-Warning "$logLead : Please go here https://bitbucket.corp.alkami.net/plugins/servlet/access-tokens/manage and create a key (Personal Access Token) with project and repository read permissions." + Write-Warning "$logLead : Once you have a key, call Set-EnvironmentVariable -Name `"Checkpoint_AllAvailableRepos_ApiKey`" -StoreName User -Value ''" + Write-Warning "$logLead : This value can also be passed in on a command line parameter -ApiKey" + throw "$logLead : Must provide API key to continue" + } + + # TODO: Refactor this to a cacheable object that can be ps1xml'd as well for friendly output + $projectResponse = (Invoke-WebRequest -Uri "https://bitbucket.corp.alkami.net/rest/api/1.0/projects?limit=1000" -UseBasicParsing -Headers @{ "Authorization" = "Bearer $ApiKey"}).Content | ConvertFrom-Json + $projectList = $projectResponse.values + $projectList | ConvertTo-Json -Depth 10 | Set-Content -Path (Join-Path $Path "projects.json") + + $allRepos = Invoke-JobRunner -JobInputs $projectList -ReturnObjects -Credential $currentUserCredential -AdditionalArguments $logLead,$Path,$ProjectSkipList,$RepositorySkipList,$SpecificProjectRepoSkipList,$ReplacePrefix,$ReplacePath,$hasSshConfigEntries,$sshConfigEntries,$ApiKey -ScriptBlock { + param( + $project, + $logLead,$Path,$ProjectSkipList,$RepositorySkipList,$SpecificProjectRepoSkipList,$ReplacePrefix,$ReplacePath,$hasSshConfigEntries,$sshConfigEntries,$ApiKey + ) + + # $logLead = $otherParams[0] + # $Path = $otherParams[1] + # $ProjectSkipList = $otherParams[2] + # $SpecificProjectRepoSkipList = $otherParams[3] + # $ReplacePrefix = $otherParams[4] + # $ReplacePath = $otherParams[5] + # $hasSshConfigEntries = $otherParams[6] + # $ApiKey = $otherParams[7] + + if (Any $ProjectSkipList.Where({$_ -eq $project.Key})) { + Write-Host "$logLead : Skipping project [$($project.Key)] per parameter list instruction" + continue + } + + # TODO: Change this to pull a common bb url + $projectKey = $project.Key + $repoResponse = (Invoke-WebRequest -Uri "https://bitbucket.corp.alkami.net/rest/api/1.0/projects/$($projectKey)/repos?limit=1000" -UseBasicParsing -Headers @{ "Authorization" = "Bearer $ApiKey"}).Content | ConvertFrom-Json + $repoResponse | ConvertTo-Json -Depth 10 | Set-Content -Path (Join-Path $Path "projects.$projectKey.json") + $repoList = $repoResponse.values + $repos = @() + foreach($repo in $repoList) { + if (Any $RepositorySkipList.Where({$_ -eq $repo.slug})) { + Write-Host "$logLead : Skipping repository [$($project.Key)/$($repo.slug)] per parameter list instruction" + continue + } + if ($null -ne $repo.links.clone) { + if (Any $SpecificProjectRepoSkipList.Where({$_.Key -eq $project.Key -and $_.Slug -eq $repo.slug})) { + # Helpful for skipping specific project/repo combos, and/or for skipping the design repos + Write-Host "$logLead : Skipping repository [$($project.Key)/$($repo.slug)] per parameter list instruction" + continue + } + $cloneUrl = $repo.links.clone.Where({$_.name -eq 'ssh'}).href + if ([string]::IsNullOrWhiteSpace($cloneUrl)) { + $cloneUrl = "unknown" + } + $clonePath = (Join-Path (Join-Path $Path $project.Key) $repo.slug) + $bbUrl = $cloneUrl + if ($replaceUrlParts) { + $bbUrl = $cloneUrl -replace $ReplacePrefix, $ReplacePath + } elseif ($hasSshConfigEntries) { + $uriBuilder = [System.UriBuilder]::new($cloneUrl) + $sshConfigEntry = @($sshConfigEntries.Where({$_.Hostname -eq $uriBuilder.Host}))[0] + if ($null -ne $sshConfigEntry) { + $uriBuilder.Host = $sshConfigEntry.Host + if (![string]::IsNullOrWhiteSpace($sshConfigEntry.User)) { + $uriBuilder.UserName = $null + } + if (![string]::IsNullOrWhiteSpace($sshConfigEntry.Port)) { + $uriBuilder.Port = -1 + } + $bbUrl = "$uriBuilder" # fast convert to a url + } + } + $repos += @{ key = $project.Key; slug = $repo.slug; cloneUrl = $cloneUrl; clonePath = $clonePath; bbUrl = $bbUrl } + } + } + return $repos + } + + # Write-Output $allRepos + $allRepos | ConvertTo-Json -Depth 10 | Set-Content -Path (Join-Path $Path "repos.json") + + $allProjectFolders = (Get-ChildItem $Path -Directory) + foreach($project in $allProjectFolders) { + $projectName = $project.Name + $projectRepos = @($allRepos.Where({ $_.Key -eq $projectName })) + if ($projectRepos.Length -eq 0) { + "Remove-Item $($project.FullName) -Recurse -Force" | Add-Content -Path $removalFilePath + continue + } + $allRepoFolders = (Get-ChildItem $project.FullName -Directory) + foreach($repo in $allRepoFolders) { + $repoName = $repo.Name + $foundRepo = @($projectRepos.Where({ $_.Slug -eq $repoName })) + if ($foundRepo.Length -eq 0) { + $movedMaybe = @($allRepos.Where({$_.Slug -eq $repoName})) + if ($movedMaybe.Length -gt 0) { + foreach($maybe in $movedMaybe) { + $maybePath = (Join-Path $Path (Join-Path $maybe.Key $maybe.Slug)) + if (Test-Path $maybePath) { + "# Did $repoName move to here? $maybePath" | Add-Content -Path $removalFilePath + } + } + } + "Remove-Item $($repo.FullName) -Recurse -Force" | Add-Content -Path $removalFilePath + } + } + } + + Invoke-JobRunner -JobInputs $allRepos -ReturnObjects -Credential $currentUserCredential -AdditionalArguments $Path,$BranchName,$branchNameEmpty,$removalFilePath -ScriptBlock { + param($repo,$Path,$BranchName,$branchNameEmpty,$removalFilePath) + $directoryName = $repo.key + $slug = $repo.slug + $projectDirectory = $repo.clonePath + $bbUrl = $repo.bbUrl + $cloneUrl = $repo.cloneUrl + $currentTime = [DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss") + if(!(Test-IsGitFolderRoot $projectDirectory)) { + (New-Item -ItemType Directory -Path $projectDirectory -Force) | Out-Null + Write-Host "git clone $bbUrl $projectDirectory" + (git clone $bbUrl $projectDirectory) | Out-Null + } else { + (Set-Location $projectDirectory) | Out-Null + if (@(git diff --stat).length) { + (git add -A . --quiet) | Out-Null + (git stash --quiet) | Out-Null + (git reset --hard --quiet) | Out-Null + } + (git remote set-url origin $bbUrl --quiet) | Out-Null + (git fetch origin --quiet) | Out-Null + $branches = (Get-GitBranchNames -Path $projectDirectory) + $checkoutBranchName = "develop" + if ((!$branchNameEmpty) -and ($branches -contains $BranchName)) { + $checkoutBranchName = $BranchName + } else { + foreach($branch in $branches) { + if ($branches -contains $branch) { + $checkoutBranchName = $branch + continue + } + } + } + (git checkout $checkoutBranchName --quiet) | Out-Null + (git pull origin $checkoutBranchName --quiet) | Out-Null + } + if ((Get-ChildItem -Path $projectDirectory).Count -eq 0) { + # Folder is empty + "# Could not find any files in $projectDirectory" | Add-Content -Path $removalFilePath + } + return "$currentTime | $projectDirectory | $directoryName | $slug | $bbUrl | $cloneUrl" + } | Add-Content -Path $pathReferencePath + + foreach($folder in $allRepos.clonePath) { + if ((Get-ChildItem -Path $folder).Count -eq 0) { + # Folder is empty + Write-Host "Could not find any files in $folder" + } + } + + Set-Location $currentLocation +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Checkpoint-EC2Instances.ps1 b/Modules/Cole.PowerShell.Developer/Public/Checkpoint-EC2Instances.ps1 new file mode 100644 index 0000000..b19245a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Checkpoint-EC2Instances.ps1 @@ -0,0 +1,52 @@ +function Checkpoint-EC2Instances { +<# +.SYNOPSIS + Get the EC2 instances for the specified profile and save them in the cached files + +.PARAMETER ProfileName + A valid ProfileName +#> + [CmdletBinding()] + [OutputType([object[]])] + param( + [Parameter(Mandatory = $true)] + [string]$ProfileName + ) + + $activityLabel = "Gathering instances" + + Assert-ValidAWSProfileName -ProfileName $ProfileName + + if (!(Test-IsCurrentAWSUserSessionValid -ProfileName $ProfileName)) { + throw "User session is invalid. Please run Update-AWSProfile $($ProfileName.Replace('temp-',''))" + } + + $PercentComplete = 1 + Write-ProgressHelper -Activity $activityLabel -Status "Starting" -PercentComplete $PercentComplete + $PercentComplete += 1 + + $allInstances = @() + foreach ($region in (Get-AWSRegions)) { + Write-ProgressHelper -Activity $activityLabel -Status $Region -PercentComplete $PercentComplete + $instances = Get-EC2Instance -Region $region -ProfileName $ProfileName + $PercentComplete += 20 + Write-ProgressHelper -Activity $activityLabel -Status $Region -PercentComplete $PercentComplete + + $allInstances += ConvertFrom-EC2Instance -Instances $instances.Instances + + $PercentComplete += 4 + Write-ProgressHelper -Activity $activityLabel -Status $Region -PercentComplete $PercentComplete + } + + $ProfileName = $ProfileName.Replace("temp-","").ToLower() + + Write-ProgressHelper -Activity $activityLabel -Status "Saving instance file" -PercentComplete 98 + $cacheFile = Get-CachePathEC2Instance -ProfileName $ProfileName + ConvertTo-Json $allInstances -Depth 10 | Set-Content -Path $cacheFile + + Write-ProgressHelper -Activity $activityLabel -Status "Saving designation file" -PercentComplete 99 + $cacheFile = Get-CachePathDesignations -ProfileName $ProfileName + $allInstances.Designation | Sort-Object | Get-Unique | Set-Content -Path $cacheFile + + Write-Progress -Activity $activityLabel -Completed +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Clear-AWSSQSQueue.ps1 b/Modules/Cole.PowerShell.Developer/Public/Clear-AWSSQSQueue.ps1 new file mode 100644 index 0000000..120eac1 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Clear-AWSSQSQueue.ps1 @@ -0,0 +1,36 @@ +function Clear-AWSSQSQueue { +<# +.SYNOPSIS + Clears all messages from AWS Queues specified by the user. + +.DESCRIPTION + +.PARAMETER MasterConnectionString + +.LINK + Runbook: https://confluence.alkami.com/x/rbBiDg + +.EXAMPLE + oo +#> + + [CmdletBinding(DefaultParameterSetName = 'QueuePrefix')] + param( + [Parameter(Mandatory = $false, ParameterSetName = "QueueName")] + [string]$QueueName = "", + + [Parameter(Mandatory = $false, ParameterSetName = "QueuePrefix")] + [string]$QueuePrefix = "", + + [Parameter(Mandatory = $false, ParameterSetName = "AllMatchedQueues")] + [switch]$AllMatchedQueues + ) + + DynamicParam { + return (Get-AwsStandardDynamicParameters) + } + + process { + Write-Host $Profile + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Clear-AlkamiModules.ps1 b/Modules/Cole.PowerShell.Developer/Public/Clear-AlkamiModules.ps1 new file mode 100644 index 0000000..894536a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Clear-AlkamiModules.ps1 @@ -0,0 +1,36 @@ +function Clear-AlkamiModules { +<# +.SYNOPSIS + Use this to unload all the Alkami modules and non-module script files +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + function Select-ModulesForDevelopmentUnloading { + <# + .SYNOPSIS + Filter the module list passed in to return modules to remove based on script-only or alkami name matching + #> + param( + [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)] + $Module = @() + ) + process { + if ($Module.Name -match 'Alkami') { + return $Module + } + + if ($Module.Name -eq 'Cole.PowerShell.Developer') { + return $Module + } + + if ($Module.Version -eq '0.0' -and $Module.ModuleType -eq 'script' -and @($Module.ExportedCommands.Keys).length -eq 0) { + return $Module + } + } + } + + (Get-Module) | Select-ModulesForDevelopmentUnloading | Remove-Module -Force +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Clear-LocalUserCredentials.ps1 b/Modules/Cole.PowerShell.Developer/Public/Clear-LocalUserCredentials.ps1 new file mode 100644 index 0000000..40f688e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Clear-LocalUserCredentials.ps1 @@ -0,0 +1,10 @@ +function Clear-LocalUserCredentials { +<# +.SYNOPSIS + Used to clear local user credentials + Even tho they are stored in a secure string, best to wipe them sometimes +#> + Remove-EnvironmentVariable -Name "CREDENTIAL_USERNAME" -Store User + Remove-EnvironmentVariable -Name "CREDENTIAL_LASTCHANGED" -Store User + Remove-EnvironmentVariable -Name "CREDENTIAL_PASSWORD" -Store User +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Clear-OldSqlLogs.ps1 b/Modules/Cole.PowerShell.Developer/Public/Clear-OldSqlLogs.ps1 new file mode 100644 index 0000000..3ae5172 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Clear-OldSqlLogs.ps1 @@ -0,0 +1,50 @@ +function Clear-OldSqlLogs { +<# +.SYNOPSIS + Will delete all files older than one month from today from any SQL Server installed on the computer + Warning: This method does not care about the file contents, it will try to delete it if it is older than 1 month. + If you use the WhatIfPreference it will try to honor that and not delete things, but I have not tested it with -WhatIf and -Force + This enumerates all subfolders called MSSQL under C:\Program Files\Microsoft SQL Server\ (the typical location and name for SQL server installs) and looks for a Log folder under that + If you have installed in a non-standard way, this will not help. + This function is NOT polite. Only use it if you are sure you want to delete things brutally. + +.PARAMETER Force + Force the file deletion +#> + [CmdletBinding()] + param ( + [switch]$Force + ) + + Write-Host "This function may have errors if you do not have read or modify permissions on the appropriate files in this folder. Please reach out to cbrand to update this file." + + $MSSqlFolders = Get-ChildItem 'C:\Program Files\Microsoft SQL Server\' -Include 'MSSQL' -Recurse + + $oneMonthAgo = [System.DateTime]::Now.AddMonths(-1) + + foreach ($msSqlFolder in $MSSqlFolders) { + $folderPath = $msSqlFolder.FullName + Write-Host "Found $folderPath" + $logsFolder = Join-Path -Path $folderPath -ChildPath 'Log' + if (Test-Path -Path $logsFolder) { + Write-Host "Found a logs folder at [$logsFolder]. Will attempt to clean it." + + $SqlServerLogFiles = Get-ChildItem -Path $logsFolder + + $oldFiles = $SqlServerLogFiles.Where({ $_.LastWriteTime -lt $oneMonthAgo }) + + foreach ($file in $oldFiles) { + $fullName = $file.FullName + try { + Write-Host "Will remove file at $fullName" + Remove-FileSystemItem -Path $fullName -WhatIf:$WhatIfPreference -Force:$Force + } catch { + Write-Warning "Was not able to delete the file at [$fullName]. You may not have permission." + Write-Warning $_.Exception.Message + } + } + } else { + Write-Host "Did not find or could not access $logsFolder" + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Close-TeamCityBlock.ps1 b/Modules/Cole.PowerShell.Developer/Public/Close-TeamCityBlock.ps1 new file mode 100644 index 0000000..d808976 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Close-TeamCityBlock.ps1 @@ -0,0 +1,89 @@ +function Close-TeamCityBlock { +<# +.SYNOPSIS + Close the TeamCity block + +.DESCRIPTION + Used to write a handy marker for content collapsing. + Note that TeamCity does not honor the name of the closed block as a developer might, so the first close that occurs after any open will close that opening block. + This is tricky to troubleshoot and will make your life frustrating when you do not manage your opening and closing as tightly as you can. + Using the pair of named functions makes it much easier to see that you have indeed opened and closed the block as expected. + +.PARAMETER Name + The name of the block being opened. + Think "collapsed" + +.PARAMETER Description + The description to show in the UI for more detail. + Think "expanded" + +.PARAMETER WasSanitized + Skips sanitizing inputs. + Only provided for the use-case of allowing the block returned object from Open-TeamCityBlock to be splatted as: + Close-TeamCityBlock @block + +.OUTPUTS + Returns an object that can be used in pipeline formation to close the block later + +.LINK + Open-TeamCityBlock + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock -Block $block + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock @block + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + $block | Close-TeamCityBlock + +.EXAMPLE + Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" +Note that this example will emit a block-opened object into the pipeline +#> + [CmdletBinding(DefaultParameterSetName = 'Message')] + [OutputType([void])] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Message')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'Message')] + [string]$Description, + [Parameter(Mandatory = $false, ParameterSetName = 'Message')] + [switch]$WasSanitized, + [Parameter(Mandatory = $true, ParameterSetName = 'Block', ValueFromPipeline = $true)] + [object]$Block + ) + + $logLead = Get-LogLeadName + + $sanitizedName = "" + $sanitizedDescription = "" + + if ($PSCmdlet.ParameterSetName -eq 'Block') { + if ($Block.WasSanitized) { + $sanitizedName = $Block.Name + $sanitizedDescription = $Block.Description + } else { + $sanitizedName = ConvertTo-SafeTeamCityMessage -InputText $Block.Name + $sanitizedDescription = ConvertTo-SafeTeamCityMessage -InputText $Block.Description + } + } else { + if ($WasSanitized) { + $sanitizedName = $Name + $sanitizedDescription = $Description + } else { + $sanitizedName = ConvertTo-SafeTeamCityMessage -InputText $Name + $sanitizedDescription = ConvertTo-SafeTeamCityMessage -InputText $Description + } + } + + if (Test-IsTeamCityProcess) { + Write-Host "##teamcity[blockClosed name='$sanitizedName' description='$sanitizedDescription']" + } else { + Write-Host "$logLead : CloseBlock $sanitizedName : $sanitizedDescription" + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Compare-File.ps1 b/Modules/Cole.PowerShell.Developer/Public/Compare-File.ps1 new file mode 100644 index 0000000..e518863 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Compare-File.ps1 @@ -0,0 +1,250 @@ +function Compare-File { +<# +.SYNOPSIS + Compares two files, displaying differences in a manner similar to traditional console-based diff utilities. +#> + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $CompareFrom, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $CompareTo, + [Parameter(Mandatory = $false)] + [int]$SurroundingLines = 5, + [switch]$Porcelain + ) + + $CompareFrom = (Resolve-Path $CompareFrom).Path + $CompareTo = (Resolve-Path $CompareTo).Path + + $totalFormatWidth = 14 # 5 x2 for numbers, 4 for column separators + $screenWidth = Get-ConsoleDisplayWidth + $fixedWidth = ($screenWidth - ($screenWidth % 2) - $totalFormatWidth) / 2 + $fileHeaderPrint = " " + + $fileHeaderFrom = Select-RightSubstringWithPadLeft -String $CompareFrom -Length ($screenWidth - 10) + $fileHeaderTo = Select-RightSubstringWithPadLeft -String $CompareTo -Length ($screenWidth - 10) + + ## Get the content from each file + $contentFrom = Get-Content -Path $CompareFrom + $contentTo = Get-Content -Path $CompareTo + + ## Compare the two files. Get-Content annotates output objects with + ## a 'ReadCount' property that represents the line number in the file + ## that the text came from. + $comparedLines = Compare-Object -ReferenceObject $contentFrom -DifferenceObject $contentTo -IncludeEqual | Sort-Object { $line.InputObject.ReadCount } + + if ($comparedLines.Count -eq 0) { + if (!$Porcelain) { + "$fileHeaderPrint : $fileHeaderFrom : $fileHeaderTo" + Write-Host "Contents were the same" + } + return + } + + $shortestFileLength = [Math]::Max($contentFrom.Length, $contentTo.Length) + $lineNumberColor = $PSStyle.ForegroundColor.LightCyan + $reset = $PSStyle.Reset + $diffLeftColor = $PSStyle.ForegroundColor.LightRed + $diffRightColor = $PSStyle.ForegroundColor.Green +<# + $lineNumber = 0 + $fromResults = @{} + $toResults = @{} + $lineNumbers = @() + foreach ($line in $comparedLines) { + $lineNumber = $line.InputObject.ReadCount + if($line.SideIndicator -eq "=>") + { + $lineNumbers += $lineNumber + $fromResults["$lineNumber"] = $line.InputObject + } + elseif($line.SideIndicator -eq "<=") + { + $lineNumbers += $lineNumber + $toResults["$lineNumber"] = $line.InputObject + } else { + $fromResults["$lineNumber"] = $line.InputObject + $toResults["$lineNumber"] = $line.InputObject + } + } +#> + + $compareStart = $comparedLines[0].InputObject.ReadCount + $compareEnd = $comparedLines[-1].InputObject.ReadCount + $lineNumbers = @() + $leftLineNumber = 1 + $rightLineNumber = 1 + $printLines = @{} + $currentLineCounter = 1 + for ($i = 1; $i -le $comparedLines.Count; $i++) { + $lines = $comparedLines.Where({$_.InputObject.ReadCount -eq $i}) + if ($null -eq $lines -or $lines.Count -eq 0) { + continue + } + $leftLine = $lines.Where({$_.SideIndicator -eq '=>'})[0].InputObject + $rightLine = $lines.Where({$_.SideIndicator -eq '<='})[0].InputObject + $sameLine = $lines.Where({$_.SideIndicator -eq '=='})[0].InputObject + $printLine = @{ Line = $currentLineCounter; LeftLine = $null; RightLine = $null; LeftLineNumber = $null; RightLineNumber = $null; } + $triggerSave = $false + if ($null -ne $leftLine) { + $printLine.LeftLine = $leftLine + $printLine.LeftLineNumber = $leftLineNumber + $triggerSave = $true + $leftLineNumber += 1 + } + if ($null -ne $rightLine) { + $printLine.RightLine = $rightLine + $printLine.RightLineNumber = $rightLineNumber + $triggerSave = $true + $rightLineNumber += 1 + } + if ($triggerSave) { + $printLines[$currentLineCounter] = $printLine + $lineNumbers += $currentLineCounter + } + if ($null -ne $sameLine) { + $triggerSave = $true + $printLine = @{ Line = $currentLineCounter; LeftLine = $sameLine; RightLine = $sameLine; LeftLineNumber = $leftLineNumber; RightLineNumber = $rightLineNumber; } + $rightLineNumber += 1 + $leftLineNumber += 1 + } + if ($triggerSave) { + $printLines[$currentLineCounter] = $printLine + $currentLineCounter += 1 + } + <#continue + if ($lines.Where({$_.SideIndicator -eq "=="}).Count -gt 0) { + $left["$i"] = $lines[0].InputObject + $right["$i"] = $lines[0].InputObject + } + if ($lines.Where({$_.SideIndicator -eq '=>'}).Count -gt 0) { + $left["$i"] = $lines.Where({$_.SideIndicator -eq '=>'})[0].InputObject + $lineNumbers += $i + } + if ($lines.Where({$_.SideIndicator -eq '<='}).Count -gt 0) { + $right["$i"] = $lines.Where({$_.SideIndicator -eq '<='})[0].InputObject + $lineNumbers += $i + }#> + } + + if ($lineNumbers.Count -gt 0) { + $lineNumbers = $lineNumbers | Sort-Object | Get-Unique + + $groups = Group-Numbers -Values $lineNumbers + <#foreach($group in $groups) { + Write-Host ($group -join ',') + }#> + $printedLineNumbers = @() + $headerBoxContent = @() + $headerBoxContent += "Left: $($fileHeaderFrom.Trim())" + $headerBoxContent += "Right: $($fileHeaderTo.Trim())" + $headerBox = Show-Box -Content $headerBoxContent -Padding 0 + $print = @($headerBox) + + $groupsCount = $groups.Count + $groupCounter = 1 + + foreach ($group in $groups) { + $first = $group[0] - $SurroundingLines + if ($first -lt 1) { + $first = 1 + } + $last = $group[-1] + $SurroundingLines + if ($last -gt $shortestFileLength) { + $last = $shortestFileLength + } + for ($i = $first; $i -le $last; $i++) { + if ($printedLineNumbers -contains $i) { + continue + } + $line = $printLines[$i] + $left = $line.LeftLine + $right = $line.RightLine + if ($lineNumbers -contains $i) { + # We need to print this as a difference + $diffLeftColor = $PSStyle.ForegroundColor.LightRed + $leftLineLead = " -" + if ([string]::IsNullOrWhiteSpace($left)) { + $leftLineLead = "" + } + $diffRightColor = $PSStyle.ForegroundColor.Green + $rightLineLead = " +" + if ([string]::IsNullOrWhiteSpace($right)) { + $rightLineLead = "" + } + } else { + # This is not a difference line, print it "normally" + $diffLeftColor = $PSStyle.Reset + $diffRightColor = $PSStyle.Reset + $leftLineLead = " " + $rightLineLead = " " + } + $lineNumberLeft = "$($line.LeftLineNumber)".PadLeft(5) + $lineNumberRight = "$($line.RightLineNumber)".PadLeft(5) + $right = "$rightLineLead$right".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $left = "$leftLineLead$left".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $print += "$lineNumberColor$lineNumberLeft$reset $diffLeftColor$left$reset :$lineNumberColor$lineNumberRight$reset $diffRightColor$right$reset" + $printedLineNumbers += $i + } + if ($groupCounter -lt $groupsCount) { + if ($last -lt ($groups[$groupCounter][0] - $SurroundingLines)) { + $print += Show-Line -Style LeftHalf + } + } + $groupCounter += 1 + } + + if ($print.Count -gt 0) { + $print + } else { + } + } else { + if (!$Porcelain) { + "$fileHeaderPrint $fileHeaderFrom : $fileHeaderTo" + Write-Host "Contents were the same" + } + return + } + +<# + $lineNumber = 0 + $fromResults = @{} + $toResults = @{} + $lineNumbers = @() + foreach ($line in $comparedLines) { + $lineNumber = $line.InputObject.ReadCount + if($line.SideIndicator -eq "=>") + { + $lineNumbers += $lineNumber + $lineOperation = "added" + $fromResults[$lineNumber] = $line.InputObject + } + elseif($line.SideIndicator -eq "<=") + { + $lineNumbers += $lineNumber + $lineOperation = "deleted" + $toResults[$lineNumber] = $line.InputObject + } + } + if ($lineNumbers.Count -gt 0) {} + $lineNumbers = $lineNumbers | Sort-Object | Get-Unique + + $print = @() + foreach ($lineNumber in $lineNumbers) { + $lineNumberLead = "$lineNumber".PadLeft(5, ' ') + $from = $fromResults[$lineNumber] + $from = "$from".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $to = $toResults[$lineNumber] + $to = "$to".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $print += "$($PSStyle.ForegroundColor.LightCyan)$lineNumberLead$($PSStyle.Reset) : $($PSStyle.ForegroundColor.LightRed)$from$($PSStyle.Reset) : $($PSStyle.ForegroundColor.Green)$to$($PSStyle.Reset)" + } + + if ($print.Count -gt 0) { + "$fileHeaderPrint : $fileHeaderFrom : $fileHeaderTo" + $print + } else { + } +#> +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Compare-Folders.ps1 b/Modules/Cole.PowerShell.Developer/Public/Compare-Folders.ps1 new file mode 100644 index 0000000..7fcb48f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Compare-Folders.ps1 @@ -0,0 +1,98 @@ +function Compare-Folders { + [CmdletBinding()] + [OutputType([string[]])] + param ( + [switch]$FilenamesOnly, + [Parameter(Position = 0)] + [string]$CompareFrom, + [Parameter(Position = 1)] + [string]$CompareTo, + [int]$SurroundingLines = 2, + [switch]$OnlyNewFrom, + [switch]$OnlyNewTo, + [string[]]$ExcludedPaths = @("bin","obj","dist","packages","TestResults","Debug","Release") + ) + + $directorySeparator = [IO.Path]::DirectorySeparatorChar + if (Test-IsWindowsPlatform) { + $directorySeparator = [IO.Path]::DirectorySeparatorChar + [IO.Path]::DirectorySeparatorChar + } +<# + $excludedPathFull = @() + foreach ($path in $ExcludedPaths) { + $excludedPathFull = "{0}$path{0}" -f $directorySeparator + } +#> + + $CompareFrom = Resolve-Path $CompareFrom + $CompareTo = Resolve-Path $CompareTo + + $filesFrom = (Get-ChildItem -Path $CompareFrom -Recurse -File) + $filesTo = (Get-ChildItem -Path $CompareTo -Recurse -File) + + if ($FilenamesOnly) { + return (Compare-Object $filesFrom $filesTo) + } + + $pathsFrom = @() + $pathsTo = @() + + # $filesFrom[22].FullName.Replace($CompareFrom,"") + # $filesFrom[22].FullName.Replace($CompareFrom,"") -split $directorySeparator + # return + + foreach ($file in $filesFrom) { + $path = $file.FullName.Replace($CompareFrom,"") + $skipAdd = $false + [string[]]$split = ($path -split $directorySeparator) + if (([System.Linq.Enumerable]::Intersect($split,$ExcludedPaths)).Count -gt 0) { + $skipAdd = $true + } + if (!$skipAdd) { + $pathsFrom += $path + } + } + + foreach ($file in $filesTo) { + $path = $file.FullName.Replace($CompareTo,"") + $skipAdd = $false + [string[]]$split = ($path -split $directorySeparator) + # if ($path.IndexOf($excludedPath) -ne -1) { + if (([System.Linq.Enumerable]::Intersect($split,$ExcludedPaths)).Count -gt 0) { + $skipAdd = $true + } + if (!$skipAdd) { + $pathsTo += $path + } + } + + $resultLines = @() + $resultFiles = @() + + if ($OnlyNewFrom) { + Compare-Object -ReferenceObject $pathsFrom -DifferenceObject $pathsTo | ? { $_.SideIndicator -eq "<="} + } elseif ($OnlyNewTo) { + Compare-Object -ReferenceObject $pathsFrom -DifferenceObject $pathsTo | ? { $_.SideIndicator -eq "=>"} + } else { + $paths = $pathsFrom + $pathsTo | Sort-Object -Unique + foreach ($file in $paths) { + $pathFromContains = $pathsFrom -contains $file + $pathToContains = $pathsTo -contains $file + $fromPath = (Join-Path $CompareFrom $file) + $toPath = (Join-Path $CompareTo $file) + if ($pathFromContains -and $pathToContains) { + if ((Get-Item -Path $fromPath).Length -ne (Get-Item -Path $toPath).Length) { + $lines = Compare-File -CompareFrom $fromPath -CompareTo $toPath -Porcelain -SurroundingLines $SurroundingLines + if ($lines.Count -gt 0) { + $resultLines += $lines + $resultLines += "`n`n" + } + } + } else { + $resultFiles += "File [$file] only present on $(if($pathFromContains) {$CompareFrom} else {$CompareTo})" + } + } + $resultFiles + $resultLines + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Convert-TypeForHelpDisplay.ps1 b/Modules/Cole.PowerShell.Developer/Public/Convert-TypeForHelpDisplay.ps1 new file mode 100644 index 0000000..2256f6b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Convert-TypeForHelpDisplay.ps1 @@ -0,0 +1,56 @@ +function Convert-TypeForHelpDisplay { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$TypeName + ) + + $TypeName = $TypeName.Trim() + + if ($TypeName.StartsWith('System.Nullable')) { + $TypeName = $TypeName.Substring(18,$TypeName.Length - 19)+"?" + } elseif ($TypeName.StartsWith('System.Collections.Generic.List`1[')) { + $TypeName = $TypeName.Substring(34, $TypeName.Length -35) + "[]" + } + + $takeLast = @( + 'System.Security.Cryptography.X509Certificates.' + 'System.Management.Automation.' + 'System.Reflection.' + 'System.ServiceProcess.' + 'System.Diagnostics.' + 'System.Collections.' + 'Microsoft.PowerShell.Commands.' + 'Microsoft.Management.Infrastructure.' + ) + + if (($TypeName -Split '\.').Count -eq 2 -and $TypeName.StartsWith('System.')) { + $TypeName = ($TypeName -split '\.')[1] + } else { + foreach ($typePrefix in $takeLast) { + if ($TypeName.StartsWith($typePrefix)) { + $TypeName = ($TypeName -split '\.')[-1] + } + } + } + + # Fix some casing + switch ($TypeName) { + 'string' { 'String' } + 'string[]' { 'String[]' } + 'string[][]' { 'String[][]' } + 'hashtable' { 'Hashtable' } + 'Int32' { 'int' } + 'Int32?' { 'int?' } + 'Int32[]' { 'int[]' } + 'Int64' { 'long' } + 'Int64?' { 'long?' } + 'Int64[]' { 'long[]' } + 'datetime' { 'DateTime' } + 'SwitchParameter' { 'switch' } + 'pscredential' { 'PSCredential' } + 'scriptblock' { 'ScriptBlock' } + default { $TypeName } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-Base64.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-Base64.ps1 new file mode 100644 index 0000000..0967f49 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-Base64.ps1 @@ -0,0 +1,14 @@ +function ConvertFrom-Base64 { +<# +.SYNOPSIS + Converts text from Base64 to UTF8 + +.PARAMETER Input + Some base64 encoded text +#> + param ( + [string]$InputObject + ) + + return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($InputObject)) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-EC2Instance.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-EC2Instance.ps1 new file mode 100644 index 0000000..978c386 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-EC2Instance.ps1 @@ -0,0 +1,90 @@ +function ConvertFrom-EC2Instance { +<# +.SYNOPSIS + Convert the results of Get-EC2Instance to a smaller data size + +.PARAMETER Instances + One or more EC2Instance results +#> + [CmdletBinding()] + param ( + [Parameter()] + [object[]]$Instances + ) + + $activityLabel = "Gathering instance information" + + if ($null -eq $Instances) { + return + } + + if ($null -ne $Instances.Instances) { + # The Get-EC2Instance call returns a nested object called Instances + # We only want that information tho + $Instances = $Instances.Instances + } + + $allInstances = @() + $instancesProcessed = 0 + foreach ($instance in $Instances) { + $innerPercentComplete = [Math]::Floor(100 * $instancesProcessed / $Instances.Count) + Write-ProgressHelper -Activity $activityLabel -Status $instance.InstanceId -PercentComplete $innerPercentComplete + $tags = @{} + foreach ($tag in $instance.Tags) { + $tags[$tag.Key] = $tag.Value + } + $designationTag = 'designation' + foreach($tagname in @('box','lane','pod','designation')) { + $designation = $tags["alk:$tagname"] + if (![string]::IsNullOrWhiteSpace($designation)) { + $designationTag = $tagname + break + } + } + $networkInterfaces = @() + foreach ($networkInterface in $instance.NetworkInterfaces) { + $networkInterfaces += @{ + Groups = $networkInterface.Groups + Ipv6Addresses = $networkInterface.Ipv6Addresses + MacAddress = $networkInterface.MacAddress + PrivateDnsName = $networkInterface.PrivateDnsName + PrivateIpAddress = $networkInterface.PrivateIpAddress + PrivateIpAddresses = $networkInterface.PrivateIpAddresses + SubnetId = $networkInterface.SubnetId + VpcId = $networkInterface.VpcId + } + } + $hostname = $instance.Tags.Where({$_.Key -eq "alk:hostname"}).Value + if (![string]::IsNullOrWhiteSpace($hostname)) { + $hostname = "$hostname.fh.local" + } + $allInstances += @{ + AvailabilityZone = $instance.Placement.AvailabilityZone + CaptureState = $instance.State.Name.Value + CpuOptions = $instance.CpuOptions + EnaSupport = $instance.EnaSupport + IamInstanceProfile = $instance.IamInstanceProfile.Arn + ImageId = $instance.ImageId + InstanceId = $instance.InstanceId + InstanceType = $instance.InstanceType.Value + NetworkInterfaces = $networkInterfaces + PrivateDnsName = $instance.PrivateDnsName + PrivateIpAddress = $instance.PrivateIpAddress + PublicDnsName = $instance.PublicDnsName + PublicIpAddress = $instance.PublicIpAddress + Region = $region + SecurityGroups = $instance.SecurityGroups | ForEach-Object { @{ GroupId = $_.GroupId; GroupName = $_.GroupName; }} + SubnetId = $instance.SubnetId + VpcId = $instance.VpcId + Tags = $tags + Hostname = $hostname + Designation = $designation + DesignationTag = $designationTag + Role = $instance.Tags.Where({$_.Key -eq "alk:role"}).Value + Service = $instance.Tags.Where({$_.Key -eq "alk:service"}).Value + } + $instancesProcessed += 1 + } + Write-Progress -Activity $activityLabel -Completed + return ($allInstances | ConvertTo-Instance) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1 new file mode 100644 index 0000000..75e754e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1 @@ -0,0 +1,54 @@ +function ConvertFrom-FailureActions { + param ( + [byte[]]$FailureActions + ) + begin { + $logLead = (Get-LogLeadName) + + $defaultTypeName = 'ServiceFailureAction' + $defaultKeys = @('ActionTypes') + $defaultDisplaySet = @('ResetPeriod','ActionTypes','Command','RebootMessage') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + if ($null -eq $FailureActions) { + return $null + } + + $actionLookup = ('None','Restart','Reboot','RunCommand') + $parsedResetPeriod = [System.BitConverter]::ToInt32($FailureActions[0..3],0) + $parsedRebootMsg = [System.BitConverter]::ToInt32($FailureActions[4..7],0) + $parsedCommand = [System.BitConverter]::ToInt32($FailureActions[8..11],0) + $parsedActions = [System.BitConverter]::ToInt32($FailureActions[12..15],0) + $parsedActionPointer = [System.BitConverter]::ToInt32($FailureActions[16..19],0) + $lpsaActionsStart = $parsedActionPointer + + $actions = @() + for($i = 0; $i -lt $parsedActions; $i++) { + $actionType = $actionLookup[[System.BitConverter]::ToInt32($FailureActions[$lpsaActionsStart..$($lpsaActionsStart+3)],0)] + $lpsaActionsStart += 4 + $actionDelay = [System.BitConverter]::ToInt32($FailureActions[$lpsaActionsStart..$($lpsaActionsStart+3)],0) + $lpsaActionsStart += 4 + if ($actionType -ne 'None') { + $actions += @{ + Type = $actionType + Delay = [System.TimeSpan]::FromSeconds($actionDelay) + } + } + } + + $properties = @{ + Actions = $actions + ActionTypes = $actions.Type -join ',' + Command = $parsedCommand + ResetPeriod = [System.TimeSpan]::FromSeconds($parsedResetPeriod) + RebootMessage = $parsedRebootMsg + } + $failureAction = New-Object PSCustomObject -Property $properties + $failureAction.PSObject.TypeNames.Insert(0,$defaultTypeName) + $failureAction | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + return $failureAction + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1xml new file mode 100644 index 0000000..01800da --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-FailureActions.ps1xml @@ -0,0 +1,55 @@ + + + + + ServiceFailureActions + + ServiceFailureAction + + + + + + DefaultServiceFailureActionView + + ServiceFailureActions + + + + + + + + + + + + + + + + + + + + + + + ResetPeriod + + + ActionTypes + + + Command + + + RebootMessage + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-SlnFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-SlnFile.ps1 new file mode 100644 index 0000000..3665ddb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertFrom-SlnFile.ps1 @@ -0,0 +1,135 @@ +function ConvertFrom-SlnFile { +<# +.SYNOPSIS + Reads in a solution file and returns a useful output object +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $Path + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-Path -Path $Path)) { + Write-Warning "$logLead : Can not find the file specified at [$Path]. Please verify it exists and is accessible." + return + } + + if (!$Path.EndsWith('.sln')) { + if ((Get-Item -Path $Path).PSIsContainer) { + $paths = (Get-ChildItem -Path (Join-Path $Path "*.sln" -Recurse)) + $pathsCount = $paths.Count + if ($pathsCount -gt 0) { + $Path = $paths[0] + if ($pathsCount -eq 1) { + Write-Host "$logLead : Found the following file under the specified folder. Using this file [$Path]." + } else { + Write-Host "$logLead : Found $($pathsCount) paths in subfolder. Will process only the first record at [$Path]." + } + } else { + Write-Warning "$logLead : Found no sln folders in file. Can not do anything. Returning null." + return $null + } + } else { + Write-Warning "$logLead : Path [$Path] does not end with [.sln] and is not a folder, may not parse properly" + } + } + + $solutionPath = (Split-Path -Path $Path -Parent) + + $lines = (Get-Content -Path $Path) + + if ($lines.Count -eq 0) { + Write-Error "$logLead : No contents found in [$Path]. Can not continue." + return $null + } + + $solutionFile = @{ Projects = @(); } + + $parsingProject = $false + $parsingProjectSection = $false + $parsingGlobal = $false + $currentProject = @{ Name = ''; Type = ''; Path = ''; Guid = ''; } + foreach ($line in $lines) { + $line = $line.Trim() + if ($line.StartsWith('#') -or [string]::IsNullOrWhiteSpace($line)) { + # Skip the comment and blank lines, we don't care + } elseif ($line.StartsWith("Project(")) { + # Parse project line + $parsingProject = $true + # example line + # Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alkami.Admin.FicoScore", "Alkami.Admin.FicoScore\Alkami.Admin.FicoScore.csproj", "{E1475E57-C4FB-401E-9404-1B48BD473C5C}" + # Project(clsid) = projectName, projectPath (from solution root), projectGuid + $splits = $line -split ',' + if ($splits.Count -ne 3) { + Write-Warning "$logLead : An unexpected (found: $($splits.Count), expected: 3) number of segments were found for this line [$line]. Expected format: [Project(clsid) = projectName, projectPath, projectGuid]. Will attempt to process." + } + $projectClsidAndName = $splits[0].Trim().Replace('"','').Replace('{','').Replace('}','') + $projectRelativePath = $splits[1].Trim().Replace('"','') + $projectGuid = $splits[2].Trim().Replace('"','').Replace('{','').Replace('}','') + + $projectClsidAndNameSplits = $projectClsidAndName -split('=') + $projectClsid = $projectClsidAndNameSplits[0].Trim().Replace('Project(','').Replace(')','') + $projectName = $projectClsidAndNameSplits[1].Trim() + $projectType = (Get-DotNetProjectFileTypeFromGuid -ProjectTypeGuid $projectClsid) + + if ($projectType -eq 'Solution Folder') { + # there is no path here + } else { + # there is a path here + $fullProjectPath = (Join-Path -Path $solutionPath -ChildPath $projectRelativePath) + $currentProject.Path = $fullProjectPath + } + $currentProject.Name = $projectName + $currentProject.Guid = $projectGuid + $currentProject.Type = $projectType + } elseif ($line.StartsWith("EndProject")) { + # End parsing a project + if (![string]::IsNullOrWhiteSpace($currentProject.Name)) { + $solutionFile.Projects += $currentProject + } + $parsingProject = $false + $currentProject = @{ Name = ''; Type = ''; Path = ''; Guid = ''; } + } elseif ($line -eq "Global") { + # Start parsing global configuration data + $parsingGlobal = $true + } elseif ($line -eq "EndGlobal") { + # End parsing global configuration data + $parsingGlobal = $false + } elseif ($line.StartsWith("VisualStudioVersion")) { + # Parse global configuration data + $solutionFile.VisualStudioVersion = $line.Split('=')[0].Trim() + } elseif ($line.StartsWith("MinimumVisualStudioVersion")) { + # Parse global configuration data + $solutionFile.MinimumVisualStudioVersion = $line.Split('=')[0].Trim() + } elseif ($line.StartsWith("Microsoft Visual Studio Solution File")) { + # Parse global configuration data + $solutionFile.SolutionFileVersion = $line.Split(',')[1].Trim() + } elseif ($parsingProject) { + # Parse project information + if ($line.StartsWith('ProjectSection')) { + $parsingProjectSection = $true + $currentProject.ProjectFiles = @() + } elseif ($line.StartsWith('EndProjectSection')) { + $parsingProjectSection = $false + } elseif ($parsingProjectSection) { + $splits = $line -split '=' + + $currentProject.ProjectFiles += @{ Name = $splits[0].Trim(); Value = $splits[1].Trim(); } + } else { + Write-Host "$logLead : Not sure how to parse this projectSection line`r`n`t$line" + } + } elseif ($parsingGlobal) { + # Parse global configuration data + # TODO: I don't need this information right now + # Still here in case I decide one day I want to capture it + } else { + Write-Host "$logLead : Not sure how to parse this line`r`n`t$line" + } + } + + return $solutionFile +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ASCII.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ASCII.ps1 new file mode 100644 index 0000000..4b7d77e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ASCII.ps1 @@ -0,0 +1,10 @@ +function ConvertTo-ASCII { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + $Text + ) + + return [System.Text.Encoding]::ASCII.GetString([System.Text.Encoding]::ASCII.GetBytes($Text)) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1 new file mode 100644 index 0000000..a91e10c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1 @@ -0,0 +1,45 @@ +function ConvertTo-AWSConfigEntry { +<# +.SYNOPSIS + Makes some neatly queryable AWSConfig entries + +.PARAMETER AWSConfigEntry + Dynamic config data +#> + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [System.Collections.Hashtable[]]$ConfigEntry + ) + begin { + $logLead = (Get-LogLeadName) + + $defaultTypeName = 'AWSConfigEntry' + $defaultKeys = @('Name') + $defaultDisplaySet = @('Name','role_arn','mfa_serial','region','source_profile') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + $return = @() + + foreach ($entry in $ConfigEntry) { + try { + $properties = @{} + foreach ($key in $entry.Keys) { + $properties[$key.Replace(' ','')] = $entry.$key + } + + $config = New-Object PSCustomObject -Property $properties + $config.PSObject.TypeNames.Insert(0,$defaultTypeName) + $config | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + $return += $config + } catch { + Write-Warning "$logLead : Could not convert item to AWSConfigEntry object. Check error below." + Write-ErrorObject -ErrorItem $PSItem + } + } + + return $return + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1xml new file mode 100644 index 0000000..d0c4a82 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSConfigEntry.ps1xml @@ -0,0 +1,61 @@ + + + + + AWSTypes + + AWSConfigEntry + + + + + + DefaultAWSConfigEntryView + + AWSTypes + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + role_arn + + + mfa_serial + + + region + + + source_profile + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1 new file mode 100644 index 0000000..4a5d56e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1 @@ -0,0 +1,45 @@ +function ConvertTo-AWSCredentialEntry { +<# +.SYNOPSIS + Makes some neatly queryable AWSCredential entries + +.PARAMETER CredentialEntry + Dynamic credential data +#> + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [System.Collections.Hashtable[]]$CredentialEntry + ) + begin { + $logLead = (Get-LogLeadName) + + $defaultTypeName = 'AWSCredentialEntry' + $defaultKeys = @('Profile') + $defaultDisplaySet = @('Profile','aws_access_key_id','toolkit_artifact_guid','region','output') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + $return = @() + + foreach ($entry in $CredentialEntry) { + try { + $properties = @{} + foreach ($key in $entry.Keys) { + $properties[$key.Replace(' ','')] = $entry.$key + } + + $credential = New-Object PSCustomObject -Property $properties + $credential.PSObject.TypeNames.Insert(0,$defaultTypeName) + $credential | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + $return += $credential + } catch { + Write-Warning "$logLead : Could not convert item to AWSCredentialEntry object. Check error below." + Write-ErrorObject -ErrorItem $PSItem + } + } + + return $return + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1xml new file mode 100644 index 0000000..e7ced23 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-AWSCredentialEntry.ps1xml @@ -0,0 +1,61 @@ + + + + + AWSCredentialEntry + + AWSCredentialEntry + + + + + + DefaultAWSCredentialEntryView + + AWSCredentialEntry + + + + + + + + + + + + + + + + + + + + + + + + + + Profile + + + aws_access_key_id + + + toolkit_artifact_guid + + + region + + + output + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Base64.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Base64.ps1 new file mode 100644 index 0000000..f1aec3f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Base64.ps1 @@ -0,0 +1,14 @@ +function ConvertTo-Base64 { +<# +.SYNOPSIS + Converts text from UTF8 to Base64 + +.PARAMETER Input + Some UTF8 encoded text +#> + param ( + [string]$InputObject + ) + + return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($InputObject)) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1 new file mode 100644 index 0000000..05564da --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1 @@ -0,0 +1,86 @@ +function ConvertTo-DriveInfo { +<# +.SYNOPSIS + Convert the CIMInstance to a defined/smaller DriveInfo (easier to serialize too) + +.OUTPUTS + Returns a [object[]] of drives. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [Microsoft.Management.Infrastructure.CimInstance]$CimInstance + ) + begin { + $logLead = (Get-LogLeadName) + + $gigabytes = (1024.0 * 1024.0 * 1024.0) + # Define the defaults for the DriveObject response to add some custom display information + $defaultTypeName = 'DriveInfo' + $defaultKeys = @('DeviceID') + $defaultDisplaySet = @('DeviceID', 'FileSystem', 'VolumeName', 'FreeSpaceGB', 'SizeGB') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + try { + $driveObject = New-Object PSCustomObject -Property @{ + Caption = $CimInstance.Caption + Description = $CimInstance.Description + InstallDate = $CimInstance.InstallDate + Name = $CimInstance.Name + Status = $CimInstance.Status + Availability = $CimInstance.Availability + ConfigManagerErrorCode = $CimInstance.ConfigManagerErrorCode + ConfigManagerUserConfig = $CimInstance.ConfigManagerUserConfig + CreationClassName = $CimInstance.CreationClassName + DeviceID = $CimInstance.DeviceID + ErrorCleared = $CimInstance.ErrorCleared + ErrorDescription = $CimInstance.ErrorDescription + LastErrorCode = $CimInstance.LastErrorCode + PNPDeviceID = $CimInstance.PNPDeviceID + PowerManagementCapabilities = $CimInstance.PowerManagementCapabilities + PowerManagementSupported = $CimInstance.PowerManagementSupported + StatusInfo = $CimInstance.StatusInfo + SystemCreationClassName = $CimInstance.SystemCreationClassName + SystemName = $CimInstance.SystemName + Access = $CimInstance.Access + BlockSize = $CimInstance.BlockSize + ErrorMethodology = $CimInstance.ErrorMethodology + NumberOfBlocks = $CimInstance.NumberOfBlocks + Purpose = $CimInstance.Purpose + FreeSpace = $CimInstance.FreeSpace + FreeSpaceGB = [Math]::Round( ($CimInstance.FreeSpace / $gigabytes), 2) + Size = $CimInstance.Size + SizeGB = [Math]::Round( ($CimInstance.Size / $gigabytes), 2) + Compressed = $CimInstance.Compressed + DriveType = $CimInstance.DriveType + FileSystem = $CimInstance.FileSystem + MaximumComponentLength = $CimInstance.MaximumComponentLength + MediaType = $CimInstance.MediaType + ProviderName = $CimInstance.ProviderName + QuotasDisabled = $CimInstance.QuotasDisabled + QuotasIncomplete = $CimInstance.QuotasIncomplete + QuotasRebuilding = $CimInstance.QuotasRebuilding + SupportsDiskQuotas = $CimInstance.SupportsDiskQuotas + SupportsFileBasedCompression = $CimInstance.SupportsFileBasedCompression + VolumeDirty = $CimInstance.VolumeDirty + VolumeName = $CimInstance.VolumeName + VolumeSerialNumber = $CimInstance.VolumeSerialNumber + PSComputerName = $CimInstance.PSComputerName + } + + #Give this object a unique typename + $driveObject.PSObject.TypeNames.Insert(0,$defaultTypeName) + $driveObject | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + + return $driveObject + } catch { + Write-Warning "$logLead : Could not convert item to DriveInfo object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1xml new file mode 100644 index 0000000..dee0457 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-DriveInfo.ps1xml @@ -0,0 +1,63 @@ + + + + + DriveInfoTypes + + DriveInfo + + + + + + DefaultDriveInfoView + + DriveInfoTypes + + + + + + + + + + + + + + + + right + + + + right + + + + + + + + DeviceID + + + FileSystem + + + VolumeName + + + FreeSpaceGB + + + SizeGB + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hash.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hash.ps1 new file mode 100644 index 0000000..7343a31 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hash.ps1 @@ -0,0 +1,26 @@ +function ConvertTo-Hash { +<# +.SYNOPSIS + Helper to turn some object into a viable Hash + +.PARAMETER DynamicObject + A dynamic object to convert the properties from +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [object]$DynamicObject + ) + + $logLead = (Get-LogLeadName) + + Write-Warning "$logLead : You probably don't want this function (do you want ConvertTo-Hashtable instead), but that's your call I guess" + + $returnValue = @{} + + foreach ($property in $DynamicObject.PSObject.Properties) { + $returnValue[$property.Name] = $property.Value + } + + return $returnValue +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hashtable.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hashtable.ps1 new file mode 100644 index 0000000..bb75a67 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Hashtable.ps1 @@ -0,0 +1,117 @@ +function ConvertTo-Hashtable { +<# +.SYNOPSIS + Used to convert an object into a hashtable + +.EXAMPLE + $json | ConvertFrom-Json | ConvertTo-HashTable + +.EXAMPLE + [xml]$someXmlVariable | ConvertTo-HashTable + +.PARAMETER InputObject + This is the object to convert + +.PARAMETER Recursing + This indicates that we are calling ourselves, so don't convert the intermediary objects to a PSCustomObject + +.LINK + https://4sysops.com/archives/convert-json-to-a-powershell-hash-table/ +#> + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(ValueFromPipeline)] + $InputObject, + [switch]$Recursing + ) + + # Return null if the input is null. This can happen when calling the function + # recursively and a property is null + if ($null -eq $InputObject) { + return $null + } + + # We don't mess with existing hashtables + if ($InputObject -is [System.Collections.Hashtable]) { + return $InputObject + } + + $properties = [System.Collections.Hashtable]@{} + + # Let's get us some XML out of there heck yeah + if ($InputObject -is [System.Xml.XmlNode]) { + $properties = [System.Collections.Hashtable]@{} + $element = $InputObject + if ($element.HasAttributes) { + $properties.Attributes = [System.Collections.Hashtable]@{} + $attributes = $element.Attributes + foreach ($attribute in $attributes) { + $name = $attribute.Name + if (![string]::IsNullOrWhiteSpace($attribute.Prefix)){ + $name = $name.Replace("$($attribute.Prefix):","") + } + $properties.Attributes[$name] = $attribute.Value + } + } + if (!$element.IsEmpty) { + # This should handle "innerText only" nodes as well + if ($element.HasChildNodes) { + $nodes = $element.ChildNodes + foreach ($node in $nodes) { + #Write-Host $node + $name = $node.Name + if (![string]::IsNullOrWhiteSpace($node.Prefix)){ + $name = $name.Replace("$($node.Prefix):","") + } + if ($name -eq '#text') { + $properties["#text"] = $element.InnerText + } else { + $properties[$name] = (ConvertTo-Hashtable $node -Recursing) + } + } + } else { + # Was the next line but discovered those get handled above easily + # $properties["#text"] = $element.InnerText + } + } + if (![string]::IsNullOrWhiteSpace($element.BaseURI)) { + $properties["BaseURI"] = $element.BaseURI + } + if (![string]::IsNullOrWhiteSpace($element.NamespaceURI)) { + $prefix = "schema" + if (![string]::IsNullOrWhiteSpace($element.Prefix)) { + $prefix = $element.Prefix + } + $properties[$element.Prefix] = $element.NamespaceURI + } + } else { + # Check if the input is an array or collection. If so, we also need to convert + # those types into hash tables as well. This function will convert all child + # objects into hash tables (if applicable) + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $collection = @( + foreach ($object in $InputObject) { + (ConvertTo-Hashtable -InputObject $object -Recursing) + } + ) + + # Return the array but don't enumerate it because the object may be pretty complex + Write-Output -NoEnumerate $collection + return + } elseif ($InputObject -is [psobject]) { # If the object has properties that need enumeration + # Convert it to its own hash table and return it + foreach ($property in $InputObject.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty' -or $_.MemberType -eq 'Property'})) { + $properties[$property.Name] = (ConvertTo-Hashtable -InputObject $property.Value -Recursing) + } + } else { + # If the object isn't an array, collection, or other object, just return it and hope for the best + return $InputObject + } + } + if ($Recursing) { + return $properties + } else { + return (New-Object PSCustomObject -Property $properties) + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1 new file mode 100644 index 0000000..3575d9e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1 @@ -0,0 +1,95 @@ +function ConvertTo-HostsFileEntry { +<# +.SYNOPSIS + Convert the given record to a defined Hosts File Entry + +.OUTPUTS + Returns a [object[]] of hosts entries. +#> + [CmdletBinding(DefaultParameterSetName = 'FromPipeline')] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'FromPipeline')] + [string]$RawRecord, + [Parameter(Mandatory = $true, ParameterSetName = 'Entries')] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true, ParameterSetName = 'Entries')] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [Parameter(Mandatory = $false, ParameterSetName = 'Entries')] + [string]$Comment + ) + begin { + # Define the defaults for the HostsFileEntry response to add some custom display information + $defaultTypeName = 'HostsFileEntry' + $defaultKeys = @('IPAddress') + $defaultDisplaySet = @('IPAddress', 'Hostname', 'IsDisabled', 'Comment') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + + + $disabledKey = "#DISABLED#" + } + process { + $workingIpAddress = $IpAddress + $workingHostname = $Hostname + $workingComment = $Comment + $isDisabled = $false + + $commentSeparator = -1 + if ($PSCmdlet.ParameterSetName -eq 'FromPipeline') { + $RawRecord = $RawRecord.Trim() + + if ($RawRecord.StartsWith($disabledKey)) { + $isDisabled = $true + $RawRecord = $RawRecord.Substring($disabledKey.Length).Trim() + } + + $commentSeparator = $RawRecord.IndexOf("#") + $workingComment = "" + $keep = $false + + if ($commentSeparator -gt -1) { + $splits = $RawRecord -split '#',2 + $workingComment = $splits[1].Trim() + $RawRecord = $splits[0].Trim() + } + + if ($RawRecord.length -gt 0) { + $bits = [regex]::Split($RawRecord, "\s+") + if ($bits.count -gt 1) { + $workingIpAddress = $bits[0].Trim() + $workingHostname = $bits[1].Trim() + } + } + } + + $keep = (($workingComment -imatch 'keep') -or ($workingIpAddress -eq $null)) + $blankLine = ([string]::IsNullOrWhiteSpace($workingComment) -and [string]::IsNullOrWhiteSpace($workingIpAddress) -and ($commentSeparator -eq -1)) + + try { + $hostEntryObject = New-Object PSCustomObject -Property @{ + IpAddress = $workingIpAddress + Hostname = $workingHostname + Comment = $workingComment + Keep = $keep + BlankLine = $blankLine + IsDisabled = $isDisabled + } + + #Give this object a unique typename + $hostEntryObject.PSObject.TypeNames.Insert(0,$defaultTypeName) + $hostEntryObject | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + + return $hostEntryObject + } catch { + Write-Warning "$logLead : Could not convert item to HostEntry object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} + +Set-Alias -Name New-HostsFileEntry -Value ConvertTo-HostsFileEntry \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1xml new file mode 100644 index 0000000..6b1ab49 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-HostsFileEntry.ps1xml @@ -0,0 +1,55 @@ + + + + + HostsFileEntryTypes + + HostsFileEntry + + + + + + DefaultHostsFileEntryView + + HostsFileEntryTypes + + + + + + + + + + + + + + + + + + + + + + + IpAddress + + + Hostname + + + IsDisabled + + + Comment + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1 new file mode 100644 index 0000000..f3721b5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1 @@ -0,0 +1,68 @@ +function ConvertTo-Instance { +<# +.SYNOPSIS + Convert the Instance to something that displays neatly + +.OUTPUTS + Returns a [object[]] of instances. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [object]$Instance + ) + begin { + $logLead = (Get-LogLeadName) + + # Define the defaults for the DriveObject response to add some custom display information + $defaultTypeName = 'Instance' + $defaultKeys = @('InstanceId') + $defaultDisplaySet = @('InstanceId', 'Hostname', 'PrivateIpAddress', 'Designation', 'InstanceType', 'CaptureState', 'Region') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + + $liveStateScript = { (Get-EC2Instance -InstanceId $this.InstanceId -ProfileName (Get-LocalCachedAWSProfile) -Region $this.Region).Instances.State.Name } + } + process { + try { + $instanceObject = New-Object PSCustomObject -Property @{ + AvailabilityZone = $Instance.AvailabilityZone + CaptureState = $Instance.CaptureState.Value + CpuOptions = $Instance.CpuOptions + EnaSupport = $Instance.EnaSupport + IamInstanceProfile = $Instance.IamInstanceProfile + ImageId = $Instance.ImageId + InstanceId = $Instance.InstanceId + InstanceType = $Instance.InstanceType + NetworkInterfaces = $Instance.NetworkInterfaces + PrivateDnsName = $Instance.PrivateDnsName + PrivateIpAddress = $Instance.PrivateIpAddress + PublicDnsName = $Instance.PublicDnsName + PublicIpAddress = $Instance.PublicIpAddress + Region = $Instance.Region + SecurityGroups = $Instance.SecurityGroups + SubnetId = $Instance.SubnetId + VpcId = $Instance.VpcId + Tags = $Instance.Tags + Hostname = $Instance.Hostname + Designation = $Instance.Designation + DesignationTag = $Instance.DesignationTag + Role = $Instance.Role + Service = $Instance.Service + } + + #Give this object a unique typename + $instanceObject.PSObject.TypeNames.Insert(0,$defaultTypeName) + $instanceObject | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + $instanceObject | Add-Member -MemberType ScriptProperty -Name LiveState -Value $liveStateScript + + return $instanceObject + } catch { + Write-Warning "$logLead : Could not convert item to Instance object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1xml new file mode 100644 index 0000000..ccad980 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-Instance.ps1xml @@ -0,0 +1,73 @@ + + + + + InstanceTypes + + Instance + + + + + + DefaultInstanceView + + InstanceTypes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InstanceId + + + Hostname + + + PrivateIpAddress + + + Designation + + + InstanceType + + + CaptureState + + + Region + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1 new file mode 100644 index 0000000..f32933c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1 @@ -0,0 +1,53 @@ +function ConvertTo-JiraTeam { +<# +.SYNOPSIS + Convert the provided objects to Jira Team representation objects + +.OUTPUTS + Returns a [object[]] of Jira Teams. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + $Team + ) + begin { + $logLead = (Get-LogLeadName) + + # Define the defaults for the JiraTeam response to add some custom display information + $defaultTypeName = 'JiraTeam' + $defaultKeys = @('Name') + $defaultDisplaySet = @('Name', 'Department', 'Lead', 'Manager', 'ScrumMaster', 'Members') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + try { + $teamObject = New-Object PSCustomObject -Property @{ + Name = $team.Name + Summary = $team.Summary + Department = $team.Department + Mission = $team.Mission + Lead = $team.Lead + Members = $team.Members | ConvertTo-JiraTeamMember + Manager = $team.Manager | ConvertTo-JiraTeamMember + ScrumMaster = $team.ScrumMaster | ConvertTo-JiraTeamMember + ProductOwner = $team.ProductOwner | ConvertTo-JiraTeamMember + ProjectManager = $team.ProjectManager | ConvertTo-JiraTeamMember + Links = $team.Links + } + + #Give this object a unique typename + $teamObject.PSObject.TypeNames.Insert(0,$defaultTypeName) + $teamObject | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + + return $teamObject + } catch { + Write-Warning "$logLead : Could not convert item to JiraTeam object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1xml new file mode 100644 index 0000000..adbbeee --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeam.ps1xml @@ -0,0 +1,66 @@ + + + + + JiraTeamTypes + + JiraTeam + + + + + + DefaultJiraTeamView + + JiraTeamTypes + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name + + + Department + + + Lead + + + $_.Manager.Name -join ',' + + + $_.ScrumMaster.Name -join ',' + + + $_.Members.Name -join ',' + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1 new file mode 100644 index 0000000..77a8c08 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1 @@ -0,0 +1,46 @@ +function ConvertTo-JiraTeamMember { +<# +.SYNOPSIS + Convert the provided objects to Jira Team Member representation objects + +.OUTPUTS + Returns a [object[]] of TeamMember. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + $TeamMember + ) + begin { + $logLead = (Get-LogLeadName) + + # Define the defaults for the DriveObject response to add some custom display information + $defaultTypeName = 'JiraTeamMember' + $defaultKeys = @('Name') + $defaultDisplaySet = @('Name', 'Role', 'DisplayName', 'Inactive') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + try { + $teamMemberObject = New-Object PSCustomObject -Property @{ + Name = $TeamMember.Name + Role = $TeamMember.Role + DisplayName = $TeamMember.DisplayName + Inactive = $TeamMember.Inactive + } + + #Give this object a unique typename + $teamMemberObject.PSObject.TypeNames.Insert(0,$defaultTypeName) + $teamMemberObject | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + + return $teamMemberObject + } catch { + Write-Warning "$logLead : Could not convert item to JiraTeam object. Check error below. Returning `$null" + Write-ErrorObject -ErrorItem $PSItem + return $null + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1xml new file mode 100644 index 0000000..ff8afde --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-JiraTeamMember.ps1xml @@ -0,0 +1,55 @@ + + + + + JiraTeamMemberTypes + + JiraTeamMember + + + + + + DefaultJiraTeamMemberView + + JiraTeamMemberTypes + + + + + + + + + + + + + + + + + + + + + + + Name + + + Role + + + DisplayName + + + Inactive + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1 b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1 new file mode 100644 index 0000000..b541f22 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1 @@ -0,0 +1,67 @@ +function ConvertTo-ScrumTeam { +<# +.SYNOPSIS + Converts a list of teams to a list of ScrumTeam objects (better for formatting, querying). + The data is driven dynamically off either the Confluence page via Get-ScrumTeamsFromConfluence or from a cached file once retrieved. + +.PARAMETER Team + Dynamic team data. Should be a table, a-la the Confluence page. +#> + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [System.Collections.Hashtable[]]$Team + ) + begin { + $logLead = (Get-LogLeadName) + $checkedHeadersAlready = $false + $expectedHeaders = @('Team','Product Owner','Strategic Product Manager','Scrum Master','Tech Lead','Eng Manager','JIRA Board','Team Slack Channel') | Sort-Object -Unique + + $defaultTypeName = 'ScrumTeam' + $defaultKeys = @('Team') + $defaultDisplaySet = @() + foreach ($header in $expectedHeaders) { + # Remove spaces so that the property names don't have spaces. + # This makes typing property names much easier. + $defaultDisplaySet += $header.Replace(' ','') + } + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + if (!$checkedHeadersAlready) { + $headers = @($Team.Keys | Sort-Object -Unique) + $spacedHeadersEqual = ((Compare-Object -ReferenceObject $expectedHeaders -DifferenceObject $headers).Count -eq 0) + $unspacedHeadersEqual = ((Compare-Object -ReferenceObject $defaultDisplaySet -DifferenceObject $headers).Count -eq 0) + if (!$spacedHeadersEqual -and !$unspacedHeadersEqual) { + Write-Warning "$logLead : The headers provided in the input object don't match the expected (hard-coded) headers from Confluence.$([System.Environment]::NewLine)This will make output printing seem to be missing data.$([System.Environment]::NewLine)Please update the format.ps1xml and $((Format-UnderlineText "{function:ConvertTo-ScrumTeam}")) with the new Confluence table headers." + Write-Host "$logLead : [Actual Headers] : $($headers -join ',')" + Write-Host "$logLead : [Expected Headers] : $($expectedHeaders -join ',')" + $checkedHeadersAlready = $true + } else { + $checkedHeadersAlready = $true + } + } + + $return = @() + + foreach ($teamObject in $Team) { + try { + $properties = @{} + foreach ($key in $teamObject.Keys) { + $properties[$key.Replace(' ','')] = $teamObject.$key + } + + $scrumTeam = New-Object PSCustomObject -Property $properties + $scrumTeam.PSObject.TypeNames.Insert(0,$defaultTypeName) + $scrumTeam | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + $return += $scrumTeam + } catch { + Write-Warning "$logLead : Could not convert item to ScrumTeam object. Check error below." + Write-ErrorObject -ErrorItem $PSItem + } + } + + return $return + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1xml b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1xml new file mode 100644 index 0000000..d19e870 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/ConvertTo-ScrumTeam.ps1xml @@ -0,0 +1,73 @@ + + + + + ScrumTeamTypes + + ScrumTeam + + + + + + DefaultScrumTeamView + + ScrumTeamTypes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Team + + + ProductOwner + + + StrategicProductManager + + + ScrumMaster + + + TechLead + + + EngManager + + + TeamSlackChannel + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Converting Database-Connected Services.md b/Modules/Cole.PowerShell.Developer/Public/Converting Database-Connected Services.md new file mode 100644 index 0000000..6a0cfb7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Converting Database-Connected Services.md @@ -0,0 +1,297 @@ +h1. Converting Database-Connected Services To An AlkamiManifest Based Installer + +> As a followup to the work done on DEV-120482 and lessons learned, we have moved the documentation for further work here to Confluence. Please continue to check this page for improved details as you work your tickets. + +h3. Background and Business Case + +EC2 based deploys at Alkami routinely take over 2.5 hours, sometimes creeping into 5 hours. The given window for installs is 4 hours, but we need a way to decrease those times to meet our new SLAs. As part of that effort, SRE and Vanguard have been working to introduce new patterns for deployments and deployment/release management (Vanguard in concert with Release Management). + +The motto to-date has been this: "Change just one thing!" - [The Art of Troubleshooting|https://artoftroubleshooting.com/2012/01/10/change-just-one-thing/]. But now we reach the end of the road on changing one thing at a time as we feel all the other components parallelizable installation have been met with good success. With this document we are looking to finalize moving all packages to deployed to EC2 instances to use an AlkamiManifest and to be in a known package format. + +Originally there was the assumption that the installer tool would produce all the required components for installation, including nuspec, but this was in a time prior to the introduction of manifests, and required too many areas of customization with added complexity at many steps. By moving to an AlkamiManifest based installer pattern, teams will have more direct control of their destiny, at the expense of a one-time setup cost, and a minor ongoing maintenance cost which would match any other maintenance for the legacy system (renaming items, moving the locations of dlls as built, etc). Additionally, this tooling does not adequately or correctly support dotnet core based services, and can not easily be made to do so without sufficient modification of all processes involved. + +This adoption of an AlkamiManifest based installer allows the release engineering team to make decisions about what actions are minimally required, so that we can avoid longer "safety checks" imposed by choices made six or more years ago. It should be possible to speed up our processes by way of parallelization, and by introducing new tools that do not rely on the foundational need of chocolatey as an end-all be-all deployment mechanism. + +The last area of service types to focus on have been the database-connected services. There will be a few further tickets aimed at looking for legacy components that still need to be moved over to AlkamiManifest based deliverables, as these are the areas that will cause us the greatest roadblock to deploying packages more smoothly. As this transition process is completed, the release engineering team will be looking to refuse to deploy any new packages that do not conform to this standard. + +However, there is an *important callout to be noted*: Due to the way our filtering logic was written ([code here](link to repository)) there may be logic services on this list, which would not include the database associated logic. + +h3. Instructions For Conversion + +To update your Database-Connected Legacy Microservices, please use the following instructions. There is no available Visual Studio tooling as there was for Widgets and Providers, for this just run some commands on the command line. + +Each ticket will contain a message such as the following: + +> For your team <*team_name*> (ex: *AI)* we have found this list of projects that we believe need to be updated. They all seem to use the Alkami.MicroServices.Installer.Database|MasterDatabase nuget packages, and therefore should be updated to the Alkami.Installer.Services pattern. +> {code} +> Alkami.MicroServices.Aggregation.Service +> Alkami.MicroServices.Aggregation.Service.Host +> Alkami.MicroServices.AggregationProviders.Yodlee.Host +> Alkami.MicroServices.FicoScore.Service.Host +> Alkami.MicroServices.MyAccounts.Service.Host +> Alkami.MicroServices.QBO.Service.Host +> {code} +> Foreach solution+project that contains a reference to Alkami.MicroServices.Installer.Database|MasterDatabase, convert the installer to Alkami.Installer.Services and add an AlkamiManifest file. You will need to update your produced manifest to include any database related dlls. + +h1. Converting Database-Connected Services To An AlkamiManifest Based Installer + +> As a followup to the work done on DEV-120482 and lessons learned, we have moved the documentation for further work here to Confluence. Please continue to check this page for improved details as you work your tickets. + +h4. Determining that your projects need to be updated + +You can determine if your csproj uses the above package with PowerShell by navigating to a solution folder and running the following command + +{code} +$csProjs = (Get-ChildItem *.csproj -recurse); +foreach ($csProj in $csProjs) { + $packagesConfigPath = (Join-Path (Split-Path $csProj -Parent) "packages.config"); + if (Test-Path $packagesConfigPath) { + $lines = (Get-Content -Path $packagesConfigPath); + foreach ($line in $lines) { + if ($line.IndexOf('Alkami.MicroServices.Installer.') -gt -1) { + Write-Host $csProj + } + } + } +} +{code} + +h4. Steps to convert + +In each solution for the referenced projects + 1. Navigate to the folder in a console window and run {{New-AlkamiManifest -Type Service}} + ** Ensure the file looks correct for your service, make any updates as necessary. Consult [https://confluence.alkami.com/display/SRE/Alkami.Installers+-+Manifests+-+Services] for appropriate details. For logic-only installers, remove references to `` or `` segments. Work with Vanguard for appropriate direction on dependencies when not clear. + ** If you only need database access, but do not have migrations, delete the migrations related nodes and use the `` tag instead per the manifest. + 1. Remove any usages of Alkami.MicroServices.Installer.Logic from your project + ** This includes removing the prebuild event from your project + 1. Delete any of the following files\folders + ** configureInstaller.ps1 + ** config.ps1 + ** configOverrides.ps1 + 1. Add a nuspec file for your project. A template is provided below. (In keeping with the spirit of updating packages, a new target folder name is used for the package/zip-file, per the documentation on Confluence: app) + ** Ensure you have a node for your migrations dll under the `` element + ** Migrations dlls can be under the same /app root folder as your application or under a folder called `/migrations`, the tooling will look for the first matching filename in the root folder. [https://confluence.alkami.com/display/SRE/Alkami.Installers+-+Package+Structures+-+Services] + 1. Create a tools folder for (and only for) and include the chocolateyInstall/chocolateyUninstall PowerShell files (templates included below) + 1. Test by creating a new package using your new nuspec file (don't forget to supply your -version flag) + ** You should be able to install (nothing changes), uninstall (service is gone) and reinstall successfully and fairly quickly. + ** Chocolatey may "error" with red text when trying to stop the service before continuing with the install. This is unfortunately normal due to the way chocolatey works. You can prevent that by stopping the service yourself if you like. + ** You may need to close the Services console (services.msc) before testing, as this can on some environments cause issues. + 1. Create a PR for your changes + 1. Update your bamboo jobs as appropriate + +h3. How long should this changeover take? + +Manually, from start to end, approximately 15 minutes at the long-side of an estimate. At this point all teams have experience converting these projects, so speed should be fairly fast. There is some tooling at the bottom of this post that should be able to help you automate the changes so you only have to review and test/commit/PR. + +h3. Alkami Bitbucket Repositories CSProj list with filters + +This spreadsheet has all of the known csproj files as of capture date, according to repository, and may give you additional details that you can use like finding which solution a csproj filename above belongs to. There may be incorrect information which we would be happy to know about so we can resolve. + +[https://docs.google.com/spreadsheets/d/1vAz-kV0hSsYz-jEFrHIKhjXDluBmofZB9ORbk6ImUls/edit#gid=94498051]  + +[Code template here] Supplied Nuspec Template +{code:java} + + + + + + + Alkami Technology, Inc. + Alkami Technology, Inc. + https://confluence.alkami.com/display + https://www.alkami.com/files/alkamilogo75x75.png + https://www.alkami.com/files/orblicense.html + false + + + + + + + + + + + + + +{code} + +[code template here] chocolateyInstall.ps1 +{code:java} +[CmdletBinding()] Param() +process { + & C:\ProgramData\Alkami\Installer\Services\install.ps1 $PSScriptRoot; + return; +} +{code} + +[code template here] chocolateyUninstall.ps1 +{code:java} +[CmdletBinding()] Param() +process { + & C:\ProgramData\Alkami\Installer\Services\uninstall.ps1 $PSScriptRoot; + return; +} +{code} + +h3. As a unified function + +This function should be runnable from any parent directory. If run at the root of your git folder, will attempt to change ALL projects so found at any depth under. Don't forget to update your sem.ver + +> *You will have to do manual modification after this script runs.* +> +> 1. You will have to declare the new nuspec file to include your migration dlls. Due to the variation across the breadth of over 400 Alkami services based projects, there is no easy way to know how to find your specific migrations package for your specific solution. +> +> This nuspec addition is as simple as adding a line to the location of your migrations dll as built. +> +> 2. You will have to modify the created AlkamiManifest to add the line for your migrations configuration. +> +> See notes above under steps to convert for further details and links + + +h4. Modified code block from the previous installer to now handle all legacy installer types +{code:java} +function ConvertFrom-LegacyMicroserviceInstaller { +<# +.SYNOPSIS + Used to attempt to clean up a solution for installing via the new process + +.PARAMETER Path + If you provide a path, will use that as the default working folder to look for csproj under. + Defaults to the current folder and looks for all "possible" csproj to work with. + +.PARAMETER Version + If you provide a version, this will be included in the nuspec file directly. Defaults to "$version$" + +.PARAMETER UseInstallerFolder + If you pass this flag, your nuspec file will be created under a folder called Installer (this matches some teams expectations regarding Bamboo jobs) +#> + [CmdletBinding()] + param( + [string]$Path, + [string]$Version = '$version$', + [switch]$UseInstallerFolder + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + $Path = (Get-Location).Path + } + + $chocoInstallFile = @" +process { + & C:\ProgramData\Alkami\Installer\Services\install.ps1 `$PSScriptRoot; + return; +} +"@ + $chocoUninstallFile = @" +process { + & C:\ProgramData\Alkami\Installer\Services\uninstall.ps1 `$PSScriptRoot; + return; +} +"@ + + $csProjs = (Get-ChildItem -Path (Join-Path $Path "*.csproj") -Recurse) + foreach ($csProj in $csProjs) { + try { + $projectFolder = (Split-Path $csProj -Parent) + Write-Verbose "Attempting to work out of [$projectFolder]" + $packagesConfigPath = (Join-Path $projectFolder "packages.config") + if (Test-Path $packagesConfigPath) { + $lines = (Get-Content -Path $packagesConfigPath) + $foundLine = $false + $writeLines = @() + foreach ($line in $lines) { + if ($line.IndexOf('Alkami.MicroServices.Installer.') -gt -1) { + $foundLine = $true + Write-Host $line + } else { + $writeLines += $line + } + } + if (!$foundLine) { + Write-Verbose "No installer logic include found in this folder [$projectFolder]" + } else { + Write-Host "Processing [$projectFolder] for removing legacy installer logic" + Set-Content -Path $packagesConfigPath -Value $writeLines + + New-AlkamiManifest -Type Service -ProjectLocation $csproj -Destination $projectFolder -ErrorAction Ignore + + if (Test-Path -Path (Join-Path $projectFolder "Installer")) { + Remove-Item -Path (Join-Path $projectFolder "Installer") -Recurse -Force -ErrorAction SilentlyContinue | Out-Null + } + + if (Test-Path -Path (Join-Path $projectFolder "InstallerOverrides")) { + Remove-Item -Path (Join-Path $projectFolder "InstallerOverrides") -Recurse -Force -ErrorAction SilentlyContinue | Out-Null + } + + $pathLeader = "" + if ($UseInstallerFolder) { + New-Item -Path (Join-Path $projectFolder "Installer") -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null + # This lets the nuspec properly reference things from the installer folder if that's how people have their solution configured. + $pathLeader = "..\" + } + + # ensure the manifest we just created is still valid, and then get the packageId so we can use it in a string replace + $manifest = Get-PackageManifest -Path $projectFolder + $packageId = $manifest.general.element + $packageNuspec = @" + + + + $packageId + $version + $packageId + Alkami Technology, Inc. + Alkami Technology, Inc. + https://confluence.alkami.com/display + https://www.alkami.com/files/alkamilogo75x75.png + https://www.alkami.com/files/orblicense.html + false + $packageId + Alkami Technology, Inc. 2022 + + Service + + + + + + + + + + +"@ + if ($UseInstallerFolder) { + Set-Content -Path (Join-Path (Join-Path $projectFolder "Installer") "$packageId.nuspec") -Value $packageNuspec -Force + } else { + Set-Content -Path (Join-Path $projectFolder "$packageId.nuspec") -Value $packageNuspec -Force + } + $toolsPath = (Join-Path $projectFolder "tools") + if (!(Test-Path $toolsPath)) { + (New-Item -Path $toolsPath -ItemType Directory -Force -ErrorAction Ignore) | Out-Null + } + Set-Content -Path (Join-Path $toolsPath "chocolateyInstall.ps1") -Value $chocoInstallFile -ErrorAction Ignore + Set-Content -Path (Join-Path $toolsPath "chocolateyUninstall.ps1") -Value $chocoUninstallFile -ErrorAction Ignore + + # idc, delete some stuff + Remove-Item -Path (Join-Path $projectFolder "configureInstaller.ps1") -Force -ErrorAction Ignore + Remove-Item -Path (Join-Path $projectFolder "config.ps1") -Force -ErrorAction Ignore + Remove-Item -Path (Join-Path $projectFolder "configOverrides.ps1") -Force -ErrorAction Ignore + + #clear prebuild from project + $xml = [xml](Get-Content $csProj) + @($xml.Project.PropertyGroup) | % { if ($null -ne $_.PreBuildEvent) { $_.PreBuildEvent = "" } } + Save-XmlFile -xmlPath $csproj -Xml $xml + } + } else { + Write-Verbose "No packages.config found in [$projectFolder], can not convert. Expected the packages.config to be at the same level as the csproj." + } + } catch { + Write-Host "Error occurred, check logs for failure. Does your packages.config have both database and logic package includes? This function is not yet that smart to fix those." + } + } +} +{code} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Disable-HostsFileEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/Disable-HostsFileEntry.ps1 new file mode 100644 index 0000000..9a839f3 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Disable-HostsFileEntry.ps1 @@ -0,0 +1,40 @@ +function Disable-HostsFileEntry { + param ( + [Parameter()] + [string]$Hostname, + [Parameter()] + [string]$IPAddress, + [Parameter(Mandatory = $true)] + $Comment + ) + + $logLead = Get-LogLeadName + + $hostnameProvided = ![string]::IsNullOrWhiteSpace($Hostname) + $ipaddressProvided = ![string]::IsNullOrWhiteSpace($IPAddress) + + if (!$hostnameProvided -and !$ipaddressProvided) { + throw "$logLead : Must provide either the hostname or ip address to disable" + } + + $hostsEntries = Get-HostsFileAllRecords + + $disabledCounter = 0 + + foreach ($record in $records) { + # can't disable an already disabled record + if (!$record.IsDisabled) { + if (($hostnameProvided -and ($record.Hostname -eq $Hostname)) -or ($ipaddressProvided -and ($record.IpAddress -eq $IPAddress))) { + $record.IsDisabled = $true + $disabledCounter += 1 + } + } + } + + if ($disabledCounter -gt 0) { + Write-Host "$logLead : Updated $disabledCounter records. Saving." + Save-CompleteHostsFile -Records $records + } else { + Write-Host "$logLead : No records found to disable for provided values." + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Enable-HostsFileEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/Enable-HostsFileEntry.ps1 new file mode 100644 index 0000000..fe049af --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Enable-HostsFileEntry.ps1 @@ -0,0 +1,37 @@ +function Enable-HostsFileEntry { + param ( + [Parameter()] + [string]$Hostname, + [Parameter()] + [string]$IPAddress + ) + + $logLead = Get-LogLeadName + + $hostnameProvided = ![string]::IsNullOrWhiteSpace($Hostname) + $ipaddressProvided = ![string]::IsNullOrWhiteSpace($IPAddress) + + if (!$hostnameProvided -and !$ipaddressProvided) { + throw "$logLead : Must provide either the hostname or ip address to enable" + } + + $records = Get-HostsFileAllRecords + + $enabledCounter = 0 + + foreach ($record in $records) { + if ($record.IsDisabled) { + if (($hostnameProvided -and ($record.Hostname -eq $Hostname)) -or ($ipaddressProvided -and ($record.IpAddress -eq $IPAddress))) { + $record.IsDisabled = $false + $enabledCounter += 1 + } + } + } + + if ($enabledCounter -gt 0) { + Write-Host "$logLead : Updated $enabledCounter records. Saving." + Save-CompleteHostsFile -Records $records + } else { + Write-Host "$logLead : No records found to enable for provided values." + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Expand-Path.ps1 b/Modules/Cole.PowerShell.Developer/Public/Expand-Path.ps1 new file mode 100644 index 0000000..f86d70f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Expand-Path.ps1 @@ -0,0 +1,18 @@ +function Expand-Path { +<# +.SYNOPSIS + Will ensure we are using the full path. Especially handy with '~/' paths + +.PARAMETER Path + The path to be expanded. Typically used with a relative path e.g. ~/.ssh -> C:\Users\\.ssh\ +#> + [CmdletBinding()] + [OutputType([string])] + param + ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [string]$Path + ) + + return ($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatExportType.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatExportType.ps1 new file mode 100644 index 0000000..8b8dadc --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatExportType.ps1 @@ -0,0 +1,84 @@ +function Find-AssembliesThatExportType { +<# +.SYNOPSIS + Occasionally with ORB it is helpful to find all the assemblies under a given folder that expose a specific type. + +.PARAMETER ClassNameFragment + This is a partial fragment or full class name. + The actual comparison looks like this: C# > type.Name.Contains(exportedTypeName) + +.PARAMETER Folder + Where to look for the DLLs. Will recurse this folder and all subdirectories. +#> + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('Name')] + [string]$ClassNameFragment, + [Parameter(Mandatory = $false)] + [Alias('Path')] + [string]$Folder = (Get-OrbPath) + ) + + # The use of the job allows us to redefine the C# code as often as we need, in case we change it while it's loaded previously + # This is just a semi-best-practice, because it does mean frequent recompilation. + # On the other hand, this is used infrequently, so ... + Start-Job -ArgumentList @( $ClassNameFragment, $Folder) -ScriptBlock { + param( + $ClassNameFragment, $Folder + ) + Add-Type -TypeDefinition @" +namespace Finder { + using System; + using System.IO; + using System.Reflection; + using System.Linq; + using System.Collections.Generic; + + public static class Finder { + public static string[] FindIt(string exportedTypeName, string targetFolder) { + var returnLines = new List(); + var files = new DirectoryInfo(@"c:\orb\webclient").GetFiles("*.dll", SearchOption.AllDirectories).Select(x => x.FullName).ToArray(); + + foreach (var file in files) { + try { + Assembly.LoadFile(file); + } catch (Exception ex) { + returnLines.Add(string.Format("Skipped Loading {0}. Reason: {1}", file, ex.Message)); + } + } + + var allLoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToArray(); + + foreach (var assembly in allLoadedAssemblies) { + Type[] exportedTypes = null; + try { + exportedTypes = assembly.GetExportedTypes(); + } catch (ReflectionTypeLoadException e) { + exportedTypes = e.Types; + } catch (Exception ex) { + if (ex.Message.Contains("does not have an implementation") || ex.Message.Contains("cannot find the file specified")) { + } else { + returnLines.Add(string.Format(" Skipped Loading {0}. Reason: {1}", assembly.FullName, ex.Message)); + } + } + + if (exportedTypes != null) { + foreach (var type in exportedTypes) { + if (type.Name.Contains(exportedTypeName)) { + returnLines.Add(string.Format(">>> {0} - {1} - {2}", assembly.FullName, type.AssemblyQualifiedName, type.FullName)); + } + } + } + } + + return returnLines.ToArray(); + } + } +} +"@ + $exportedTypeName = $ClassNameFragment + $startingFolder = $Folder + [Finder.Finder]::FindIt($exportedTypeName, $startingFolder) # | Out-String + } | Receive-Job -Wait -AutoRemoveJob +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatReferenceByFragment.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatReferenceByFragment.ps1 new file mode 100644 index 0000000..ade49af --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-AssembliesThatReferenceByFragment.ps1 @@ -0,0 +1,79 @@ +function Find-AssembliesThatReferenceByFragment { +<# +.SYNOPSIS + Occasionally with ORB it is helpful to find all the assemblies under a given folder that reference a specific fragment. + +.PARAMETER ClassNameFragment + This is a partial fragment or full class name. + The actual comparison looks like this: C# > .GetReferencedAssemblies().Where(x => x.Name.StartsWith(exportedTypeName, StringComparison.InvariantCultureIgnoreCase)) + +.PARAMETER Folder + Where to look for the DLLs. Will recurse this folder and all subdirectories. +#> + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('Name')] + [string]$ClassNameFragment, + [Parameter(Mandatory = $false)] + [Alias('Path')] + [string]$Folder = (Get-OrbPath) + ) + + # The use of the job allows us to redefine the C# code as often as we need, in case we change it while it's loaded previously + # This is just a semi-best-practice, because it does mean frequent recompilation. + # On the other hand, this is used infrequently, so ... + Start-Job -ArgumentList @( $ClassNameFragment, $Folder) -ScriptBlock { + param( + $ClassNameFragment, $Folder + ) + Add-Type -TypeDefinition @" +namespace Finder { + using System; + using System.IO; + using System.Reflection; + using System.Linq; + using System.Collections.Generic; + + public static class Finder { + public static IList FindIt(string exportedTypeName, string targetFolder) { + var returnLines = new List(); + var assIgnoreList = new List{"mscorlib"}; + + var di = new DirectoryInfo(targetFolder); + + var fileList = di.GetFiles("*.dll", SearchOption.AllDirectories).Select(x => x.FullName).ToList(); + + foreach(var file in fileList) { + try { + Assembly.LoadFrom(file); + } catch { + assIgnoreList.Add(file); + } + } + + foreach(var ass1 in AppDomain.CurrentDomain.GetAssemblies()) { + var refs = ass1.GetReferencedAssemblies().Where(x => x.Name.StartsWith(exportedTypeName, StringComparison.InvariantCultureIgnoreCase)); + if (refs.Any()) { + foreach (var ref1 in refs) { + if (returnLines.Any()) { + returnLines.Add(string.Empty); + } + //returnLines.Add(string.Format("{2} :: {0} Loads --> {1}", ass1.FullName, ref1.FullName, ass1.Location)); + returnLines.Add(ass1.Location); + returnLines.Add(string.Format(" {0}", ass1.FullName)); + returnLines.Add(string.Format(" Loads -> {0}", ref1.FullName)); + } + } + } + + return returnLines; + } + } +} +"@ + $exportedTypeName = $ClassNameFragment + $startingFolder = $Folder + [Finder.Finder]::FindIt($exportedTypeName, $startingFolder) # | Out-String + } | Receive-Job -Wait -AutoRemoveJob +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-Command.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-Command.ps1 new file mode 100644 index 0000000..5050864 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-Command.ps1 @@ -0,0 +1,57 @@ +Function Find-Command { +<# +.SYNOPSIS + Used to find a command somewhere on the system. + Recursively looks in all paths and under special [Program Files] and [Program Files (x86)] folders if defined. + Replacement of `which` command + +.PARAMETER Filename + The filename to look for. +#> + param ( + [Parameter(Mandatory = $true, Position = 0)] + $Filename + ) + + $result = (Get-Command $Filename) + + if ($null -ne $result) { + if (![string]::IsNullOrWhiteSpace($result.Path)) { + return $result.Path + } else { + return $result.Source + } + } + + if ($Filename.IndexOf('.') -eq -1) { + # add a wildcard so it will look for all matching Filename extensions + $Filename = "$Filename.*" + } + + # TODO ~ Make this parallel and change the return type + $paths = @($env:path -split ';') + $paths += $env:ProgramFiles + $paths += ${env:ProgramFiles(x86)} + + $paths = ($paths | Select-Object -Unique) + + foreach($path in $paths) { + if (![string]::IsNullOrWhiteSpace($path)) { + $foundItems = Get-ChildItem -Path (Join-Path -Path $path -ChildPath $Filename) -Recurse -ErrorAction Ignore + if ($foundItems.Count -eq 1) { + return $foundItems.FullName + } elseif ($foundItems.Count -gt 1) { + Write-Host "Found several results for $Filename" + foreach ($foundItem in $foundItems) { + Write-Host $foundItem.FullName + } + Write-Host "" + return $foundItems[0] + } + } + } + + Write-Warning "Could not find a matching file" +} + +Set-Alias -Name which -Value Find-Command \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-GitExecutablePath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-GitExecutablePath.ps1 new file mode 100644 index 0000000..c21bae5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-GitExecutablePath.ps1 @@ -0,0 +1,29 @@ +function Find-GitExecutablePath { +<# +.SYNOPSIS + Find the git executable path on this system +#> + param () + + $logLead = (Get-LogLeadName) + + $path = (Get-Command git).Source + + $isMissing = [string]::IsNullOrWhiteSpace($path) + + $defaultPaths = @('C:\Program Files\git\bin\git.exe','C:\Program Files (x86)\git\bin\git.exe') + + if (!$isMissing) { + return $path + } + + foreach ($defaultPath in $defaultPaths) { + $path = $defaultPath + if (Test-Path -Path $path) { + return $path + } + } + + # We didn't hit a return statement, therefore this is missing + throw "$logLead : Can not find an installed/in-path git command" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-GitRepoRootFromPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-GitRepoRootFromPath.ps1 new file mode 100644 index 0000000..7bb645d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-GitRepoRootFromPath.ps1 @@ -0,0 +1,38 @@ +function Find-GitRepoRootFromPath { +<# +.SYNOPSIS + Find the root repository for a given folder, if there is one. + Throws if neither the folder nor it's parents are a git repository. + +.PARAMETER Path + The file path to check. Can be presumed as the current folder. +#> + param( + $Path = ((Get-Location).Path) + ) + + $logLead = (Get-LogLeadName) + + $repoRootFound = $false + $rootFolderFound = $false + $workingFolder = (Resolve-Path $Path).Path + + do { + if (!(Test-Path $workingFolder)) { + throw "$logLead : Path does not exist or can not be evaluated at [$workingFolder]" + } + $repoRootPath = (Join-Path $workingFolder ".git") + if (Test-Path $repoRootPath) { + $repoRootFound = $true + + break + } + $newWorkingFolder = (Get-Item $workingFolder).Parent.FullName + if ([string]::IsNullOrWhiteSpace($newWorkingFolder)) { + throw "$logLead : Can not find a parent folder for [$workingFolder] looking above [$Path]" + } + $workingFolder = $newWorkingFolder + } while ($true) + + return $workingFolder +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Find-MissingReferences.ps1 b/Modules/Cole.PowerShell.Developer/Public/Find-MissingReferences.ps1 new file mode 100644 index 0000000..788d9d2 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Find-MissingReferences.ps1 @@ -0,0 +1,180 @@ +function Find-MissingReferences { +<# +.SYNOPSIS + Used to fetch all the assembly references and find which ones are missing from the folder presented by Path + +.DESCRIPTION + Will check for files that should be present in the folder but which are not. Will assume any file loaded from the GAC is acceptable to not be present in the folder. + +.PARAMETER Path + The folder where we should look for the files, or the full path of the file to be checked + +.PARAMETER EntryPoint + Specify a specific file to consider the entry point for determining what is missing. + +.PARAMETER ThrowOnError + Use this in a build environment if required + +.PARAMETER Noisy + Be noisy. Show me all the stuff with warnings and such. + +.PARAMETER Recursing + Don't set this +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $false)] + [string]$EntryPoint, + [switch]$ThrowOnError, + [switch]$Noisy, + [switch]$Recursing + ) + + $logLead = Get-LogLeadName + + if ($null -eq $global:allReferencedAssemblies) { + # You weren't supposed to set this, so if this value doesn't exist, then we can't be here + # Set the value to false so we can check it later for writing data out + $Recursing = $false + $global:allReferencedAssemblies = @() + } + if ($null -eq $global:loadedReferencesAlready) { + $global:loadedReferencesAlready = @() + } + if ($null -eq $global:missingReferences) { + $global:missingReferences = @() + } + + # early exit if we already encountered this before + if ($global:missingReferences -contains $EntryPoint) { + return + } + if ($global:loadedReferencesAlready -contains $EntryPoint) { + return + } + + # Check to see if it is in the GAC + try { + $assembly = [System.Reflection.Assembly]::Load($reference) + if ($null -ne $assembly) { + if ($Noisy) { + Write-Host "$($assembly.GetName().Name) - GAC? $($assembly.GlobalAssemblyCache) - $($assembly.Location)" + } + } + $global:loadedReferencesAlready += $assembly.GetName().Name + } catch { + # Keep going + } + + + if (!(Test-Path -Path $Path)) { + throw "$logLead : The path provided is not a valid path or does not exist." + } + + $Path = Resolve-Path $Path + + $item = Get-Item -Path $Path + + $files = @() + + if ($item.PSIsContainer) { + if ([string]::IsNullOrWhiteSpace($EntryPoint)) { + if ($Recursing) { + return + } + # We were given a folder, nut no EntryPoint, so just check all the files in the folder + $files = (Get-ChildItem (Join-Path -Path $Path "*.dll") -File).Name + } else { + if (!$EntryPoint.EndsWith(".dll")) { + $entryPointFileName = "$EntryPoint.dll" + } + $filePath = Join-Path -Path $Path -ChildPath $entryPointFileName + if (!(Test-Path $filePath)) { + $global:missingReferences += $EntryPoint + if ($Noisy) { + Write-Warning "$logLead : The file [$entryPointFileName] specified was not found in [$Path]" + } + return + } + $files = (Get-ChildItem $filePath -File).Name + } + } else { + # We were given a file directly + $EntryPoint = $item.Name + $Path = $item.Directory.FullName + $files = @($EntryPoint) + } + + foreach ($file in $files) { + $fullName = (Join-Path -Path $Path -ChildPath $file) + $assembly = [System.Reflection.Assembly]::LoadFile($fullName) + $referencedAssemblies = $assembly.GetReferencedAssemblies() + $global:loadedReferencesAlready += $assembly.Name + # [System.IO.Path]::GetFileNameWithoutExtension($fullName) + foreach ($reference in $referencedAssemblies) { + $referenceName = $reference.Name + if ([string]::IsNullOrWhiteSpace($referenceName)) { + continue + } + if ($global:loadedReferencesAlready -contains $referenceName) { + continue + } + if ($global:allReferencedAssemblies -notcontains $referenceName) { + # Write-Host "add ref $referenceName" + $global:allReferencedAssemblies += $referenceName + $discard = Find-MissingReferences -Path $Path -EntryPoint $referenceName -Recursing -Noisy:$Noisy + } + } + $global:loadedReferencesAlready += $assembly.GetName().Name + } + + if (!$Recursing) { + # cleanup the "missing" list because of GAC stuff + $missing = @() + foreach ($ref in $global:missingReferences) { + if ($global:loadedReferencesAlready -notcontains $ref) { + $missing += $ref + } + } + + if ($Noisy) { + Write-Host "Found these referenced assemblies" + if ($null -ne ${function:Show-ListAsTable}) { + Show-ListAsTable ($global:allReferencedAssemblies | Sort-Object | Get-Unique) + } else { + Write-Host (($global:missingReferences | Sort-Object | Get-Unique) -Join "`n") + } + + Write-Host "Found these available assemblies" + if ($null -ne ${function:Show-ListAsTable}) { + Show-ListAsTable ($global:loadedReferencesAlready | Sort-Object | Get-Unique) + } else { + Write-Host (($global:missingReferences | Sort-Object | Get-Unique) -Join "`n") + } + } + + if (!(Test-IsCollectionNullOrEmpty $global:missingReferences)) { + Write-Host "Found these missing references" + if ($null -ne ${function:Show-ListAsTable}) { + Show-ListAsTable ($missing | Sort-Object | Get-Unique) + } else { + Write-Host (($missing | Sort-Object | Get-Unique) -Join "`n") + } + } + + Remove-Variable -Name missingReferences -Scope Global + Remove-Variable -Name allReferencedAssemblies -Scope Global + Remove-Variable -Name loadedReferencesAlready -Scope Global + + if ($ThrowOnError) { + if (!(Test-IsCollectionNullOrEmpty $missing)) { + throw "$logLead : You're missing some required references" + } + } + + return $missing + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-BoldText.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-BoldText.ps1 new file mode 100644 index 0000000..7964b07 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-BoldText.ps1 @@ -0,0 +1,21 @@ +function Format-BoldText { +<# +.SYNOPSIS + Get text formatted for bold if the system supports it. + +.PARAMETER Text + The text to apply formatting to. +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [string]$Text + ) + + if (Test-TerminalSupportsANSIEscapes) { + return "$($PSStyle.Bold)$Text$($PSStyle.BoldOff)" + } else { + return $Text + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-HostsFileRecord.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-HostsFileRecord.ps1 new file mode 100644 index 0000000..6825f6b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-HostsFileRecord.ps1 @@ -0,0 +1,72 @@ +function Format-HostsFileRecord { +<# +.SYNOPSIS + Used to format the record object into a neatly outputted string + +.PARAMETER Records + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.OUTPUTS + One or more formatted lines to write to the hosts file +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [object[]]$Records + ) + + $logLead = Get-LogLeadName + + $lines = @() + + if (Test-IsCollectionNullOrEmpty $Records) { + throw "$logLead : no records found to write to the hosts file" + } + + if (($null -eq $Records.IpAddress) -or ($null -eq $Records.Hostname)) { + throw "$logLead : no records found with ipaddress or hostname, can not continue" + } + + $maxHostnameWidth = 0; + foreach ($record in $Records) { + if (![string]::IsNullOrWhiteSpace($record.Hostname)) { + if ($record.Hostname.Length -gt $maxHostnameWidth) { + $maxHostnameWidth = $record.Hostname.Length; + } + } + } + + if ($maxHostnameWidth -eq 0) { + return; + } else { + $maxHostnameWidth += 5; + } + + foreach ($record in $Records) { + $line = "" + if ([string]::IsNullOrWhiteSpace($record.IpAddress)) { + if ($record.BlankLine) { + $line = ""; + } else { + $line = "# $($record.Comment)" + } + } else { + $disabledPart = "" + if ($record.IsDisabled) { + # This matches the parsing component in ConvertTo-HostsFileEntry + $disabledPart = "#DISABLED# " + } + $firstPart = $record.IpAddress.PadRight(18) + $secondPart = $record.Hostname + $thirdPart = "" + if (![string]::IsNullOrWhiteSpace($record.Comment)) { + $secondPart = $secondPart.PadRight($maxHostnameWidth) # add some space before the comment + $thirdPart = "# $($record.Comment)" # prefix the comment + } + $line = "$disabledPart$firstPart$secondPart$thirdpart" + } + $lines += $line + } + + return $lines +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-TextJustified.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-TextJustified.ps1 new file mode 100644 index 0000000..7cecf1b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-TextJustified.ps1 @@ -0,0 +1,55 @@ +function Format-TextJustified { + param( + [string[]]$Words, + [int]$MaxWidth, + [switch]$ExceptLastLine + ) + + if ($Words.Count -eq 1) { + $Words = $Words -split '\s+' + } + + $preProcess = @() + $row = @{ Letters = 0; Words = @() } + foreach ($word in $Words) { + if (($row.Words.Count -gt 0) -and ($row.Letters + $row.Words.Count + $word.Length -gt $MaxWidth)) { + # adding this word to the array would make it cross the magic threshold + # Add a new row to the array and start using that. + $preProcess += $row + $row = @{ Letters = 0; Words = @() } + } + $row.Words += $word + $row.Letters += $word.Length + } + # push the final row + $preProcess += $row + + $results = @() + $rowCounter = 0 + foreach ($row in $preProcess) { + if ($row.Words.Count -eq 1 -or $rowCounter -eq $preProcess.Count) { + $results += (($row.Words -join ' ') + "".PadRight($MaxWidth - $row.Letters - $row.Words.Count + 1)) + continue + } + $rowWordCount = $row.Words.Count + $countLessOne = $rowWordCount - 1 + $spaces = $MaxWidth - $row.Letters + $minSpaces = "".PadRight(([Math]::Floor($spaces/$countLessOne))) + $addSpace = $spaces % $countLessOne + $result = $row.Words[0] + for($i = 1; $i -lt $rowWordCount; $i++) { + $extraSpaces = "" + if ($i -le $addSpace) { + $extraSpaces = ' ' + } + $word = $row.Words[$i] + $result += "$minSpaces$extraSpaces$word" + } + $results += $result + } + if ($ExceptLastLine) { + $results[-1] = ($results[-1] -split '\s+') -join ' ' + } + + return $results -join "`n" +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay.ps1 new file mode 100644 index 0000000..39e4a62 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay.ps1 @@ -0,0 +1,72 @@ +function Format-TextWrapToDisplay { +<# +.SYNOPSIS + Formats an input array of strings to no wider than the display of the screen + Keeps whole words together where possible + +.PARAMETER InputText + Input text + +.PARAMETER MaxWidth + Specify a specific fixed with. Defaults to the current console width. + +.EXAMPLE + Format-TextWrapToDisplay -InputText +#> + [CmdletBinding()] + [OutputType([string[]])] + Param( + [Parameter(Mandatory = $false)] + [Alias('Width')] + [int]$MaxWidth, + [parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('Text')] + [string[]]$InputText + ) + + $lines = @() + + if ($MaxWidth -eq 0) { + $MaxWidth = (Get-ConsoleDisplayWidth) + } + + $currentLineWords = @() + $currentLineCharCount = 0 + $wordArray = $InputText.Split((' ',"`r","`n"),[System.StringSplitOptions]::RemoveEmptyEntries) + # Write-Output $linesArray + + #foreach($line in $linesArray) { + # $lineSplits = $line.Split((' ',"`r","`n"),[System.StringSplitOptions]::RemoveEmptyEntries) + + foreach($word in $wordArray) { + $currentWordLength = $word.Length + + # We have a word that is longer than the current display width, so we need to account for that + if (($currentLineCharCount -eq 0) -and (($MaxWidth - $currentWordLength) -lt 2)) { + # add the word to the lines and continue + $lines += $word + Write-Host "triggered on too long of a word" + continue + } + + # The current line buffer is larger than the offset allowance if we add another word + # Stop and stuff it on the output buffer, then continue + if (($MaxWidth - ($currentLineCharCount + $currentWordLength)) -lt 2) { + $lines += ($currentLineWords -join ' ').Trim() + $currentLineCharCount = 0 + $currentLineWords = @() + } + + $currentLineWords += $word + + # add one to account for spaces + $currentLineCharCount += ($currentWordLength + 1) + } + + $lines += ($currentLineWords -join ' ').Trim() + $currentLineWords = @() + $currentLineCharCount = 0 + #} + + return $lines +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/LoremIpsum.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/LoremIpsum.pester.ps1 new file mode 100644 index 0000000..e1ae54a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/LoremIpsum.pester.ps1 @@ -0,0 +1,83 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +$loremIpsum = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin commodo pellentesque lorem, a vulputate nisi elementum nec. Duis a erat est. Vestibulum aliquam lorem eu tempor sodales. Phasellus ac tellus in mauris posuere venenatis quis et massa. Aenean eu enim nisi. Donec a diam dictum, posuere odio vel, sodales tortor. Vestibulum interdum ultricies tortor eget placerat. + +Morbi mollis eu quam et laoreet. Curabitur sed velit nec diam hendrerit facilisis. Cras ornare velit vel ultrices lacinia. Vivamus mollis vehicula pharetra. Nam ut libero purus. Nunc cursus, nunc sed pharetra sagittis, quam nibh mattis nisl, quis auctor risus elit ut elit. Aliquam posuere nisi eu libero condimentum egestas at id mi. Integer vel eros ac magna dapibus viverra. Ut facilisis sagittis sem, eget pretium libero semper ac. Nulla vel turpis elit. Etiam non tempus tortor, et imperdiet arcu. Nullam iaculis sapien turpis, ut congue diam tincidunt et. Proin mollis dui posuere felis facilisis, vel finibus eros pellentesque. Nulla ullamcorper augue at nunc bibendum mattis. Nam nisi risus, tempus id tellus ac, aliquam aliquam nulla. + +Suspendisse id neque quis lacus aliquam viverra. Sed pulvinar, nibh vitae congue malesuada, felis augue condimentum augue, vitae malesuada risus leo molestie elit. Cras tempus molestie dictum. Nunc non elit et velit aliquet eleifend a at dolor. Donec vel nunc ligula. Duis ac diam ipsum. Donec pellentesque purus at neque tempus, ut aliquet nisl hendrerit. +"@ + +Describe "171 characters" { + + # The use of the here-block vs the array join seems to cause the need for a triple return in the middle +$lines_171 = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin commodo pellentesque lorem, a vulputate nisi elementum nec. Duis a erat est. Vestibulum aliquam lorem eu +tempor sodales. Phasellus ac tellus in mauris posuere venenatis quis et massa. Aenean eu enim nisi. Donec a diam dictum, posuere odio vel, sodales tortor. Vestibulum +interdum ultricies tortor eget placerat. + + + +Morbi mollis eu quam et laoreet. Curabitur sed velit nec diam hendrerit facilisis. Cras ornare velit vel ultrices lacinia. Vivamus mollis vehicula pharetra. Nam ut +libero purus. Nunc cursus, nunc sed pharetra sagittis, quam nibh mattis nisl, quis auctor risus elit ut elit. Aliquam posuere nisi eu libero condimentum egestas at id +mi. Integer vel eros ac magna dapibus viverra. Ut facilisis sagittis sem, eget pretium libero semper ac. Nulla vel turpis elit. Etiam non tempus tortor, et imperdiet +arcu. Nullam iaculis sapien turpis, ut congue diam tincidunt et. Proin mollis dui posuere felis facilisis, vel finibus eros pellentesque. Nulla ullamcorper augue at nunc +bibendum mattis. Nam nisi risus, tempus id tellus ac, aliquam aliquam nulla. + + + +Suspendisse id neque quis lacus aliquam viverra. Sed pulvinar, nibh vitae congue malesuada, felis augue condimentum augue, vitae malesuada risus leo molestie elit. Cras +tempus molestie dictum. Nunc non elit et velit aliquet eleifend a at dolor. Donec vel nunc ligula. Duis ac diam ipsum. Donec pellentesque purus at neque tempus, ut +aliquet nisl hendrerit. +"@ + Mock -CommandName Get-ConsoleDisplayWidth -MockWith { return 171 } + + Context "Wraps to 171 characters" { + $result = Format-TextWrapToDisplay -InputText $loremIpsum + + It "matches" { + ($result -join [System.Environment]::NewLine) | Should -Be $lines_171 + } + } +} + +Describe "95 characters" { + + # The use of the here-block vs the array join seems to cause the need for a triple return in the middle +$lines_95 = @" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin commodo pellentesque lorem, a +vulputate nisi elementum nec. Duis a erat est. Vestibulum aliquam lorem eu tempor sodales. +Phasellus ac tellus in mauris posuere venenatis quis et massa. Aenean eu enim nisi. Donec a +diam dictum, posuere odio vel, sodales tortor. Vestibulum interdum ultricies tortor eget +placerat. + + + +Morbi mollis eu quam et laoreet. Curabitur sed velit nec diam hendrerit facilisis. Cras +ornare velit vel ultrices lacinia. Vivamus mollis vehicula pharetra. Nam ut libero purus. +Nunc cursus, nunc sed pharetra sagittis, quam nibh mattis nisl, quis auctor risus elit ut +elit. Aliquam posuere nisi eu libero condimentum egestas at id mi. Integer vel eros ac magna +dapibus viverra. Ut facilisis sagittis sem, eget pretium libero semper ac. Nulla vel turpis +elit. Etiam non tempus tortor, et imperdiet arcu. Nullam iaculis sapien turpis, ut congue +diam tincidunt et. Proin mollis dui posuere felis facilisis, vel finibus eros pellentesque. +Nulla ullamcorper augue at nunc bibendum mattis. Nam nisi risus, tempus id tellus ac, aliquam +aliquam nulla. + + + +Suspendisse id neque quis lacus aliquam viverra. Sed pulvinar, nibh vitae congue malesuada, +felis augue condimentum augue, vitae malesuada risus leo molestie elit. Cras tempus molestie +dictum. Nunc non elit et velit aliquet eleifend a at dolor. Donec vel nunc ligula. Duis ac +diam ipsum. Donec pellentesque purus at neque tempus, ut aliquet nisl hendrerit. +"@ + Mock -CommandName Get-ConsoleDisplayWidth -MockWith { return 95 } + + Context "Wraps to 95 characters" { + $result = Format-TextWrapToDisplay -InputText $loremIpsum + + It "matches" { + ($result -join [System.Environment]::NewLine) | Should -Be $lines_95 + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/ReallyLongWord.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/ReallyLongWord.pester.ps1 new file mode 100644 index 0000000..6ebdf15 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-TextWrapToDisplay/ReallyLongWord.pester.ps1 @@ -0,0 +1,29 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +$textBody = @" +This text will handle having one really long 123456789012345678901234567890 word jammed up in the middle that is 30 characters long on a screen width of 25. +"@ + +Describe "25 characters and a 30 character word" { + + # The use of the here-block vs the array join seems to cause the need for a triple return in the middle +$lines_25 = @" +This text will handle +having one really long +123456789012345678901234567890 +word jammed up in the +middle that is 30 +characters long on a +screen width of 25. +"@ + Mock -CommandName Get-ConsoleDisplayWidth -MockWith { return 25 } + + Context "Wraps to 25 characters" { + $result = Format-TextWrapToDisplay -InputText $textBody + + It "matches" { + ($result -join [System.Environment]::NewLine) | Should -Be $lines_25 + } + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-UnderlineText.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-UnderlineText.ps1 new file mode 100644 index 0000000..a2dd13a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-UnderlineText.ps1 @@ -0,0 +1,21 @@ +function Format-UnderlineText { +<# +.SYNOPSIS + Get text formatted for underline if the system supports it. + +.PARAMETER Text + The text to apply formatting to. +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [string]$Text + ) + + if (Test-TerminalSupportsANSIEscapes) { + return "$($PSStyle.Underline)$Text$($PSStyle.UnderlineOff)" + } else { + return $Text + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Format-XML.ps1 b/Modules/Cole.PowerShell.Developer/Public/Format-XML.ps1 new file mode 100644 index 0000000..a1aac0a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Format-XML.ps1 @@ -0,0 +1,14 @@ +function Format-XML { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline=$true,Mandatory=$true)] + [string]$Xmlcontent + ) + $xmldoc = New-Object System.Xml.XmlDocument + $xmldoc.LoadXml($Xmlcontent) + $sw = New-Object System.IO.StringWriter + $writer = New-Object System.Xml.XmlTextwriter($sw) + $writer.Formatting = [System.XML.Formatting]::Indented + $xmldoc.WriteContentTo($writer) + $sw.ToString() +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AWSAccessKey.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AWSAccessKey.ps1 new file mode 100644 index 0000000..8c66e86 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AWSAccessKey.ps1 @@ -0,0 +1,61 @@ +function Get-AWSAccessKey { +<# +.SYNOPSIS + Update the AWS access key and secret in a reasonable fashion + +.PARAMETER RoleToReplaceFor + The role you are replacing the key value for. Example: [teamcity-packer] or [Prod] + +.PARAMETER Key + The value given by the Access Key ID for AWS when choosing a new IAM Access Key + +.PARAMETER Secret + The value given by the secret for AWS when choosing a new IAM Access Key + +.PARAMETER ComputerName + Denotes the computers you wish to change the value on +#> + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [Alias('ProfileName')] + [string]$RoleToReplaceFor, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Alias('Servers')] + [string[]]$ComputerName = (Get-CachedInstances -ProfileName temp-prod -TeamCity).Hostname + ) + + if (-not $RoleToReplaceFor.StartsWith("[")) { + $RoleToReplaceFor = "[$RoleToReplaceFor" + } + if (-not $RoleToReplaceFor.EndsWith("]")) { + $RoleToReplaceFor = "$RoleToReplaceFor]" + } + + Write-Host "$logLead : Replacing key for profile $RoleToReplaceFor with key: $AccessKeyId" + + Invoke-Command -ComputerName $ComputerName -ArgumentList ($RoleToReplaceFor) -ScriptBlock { + param ($sb_role) + $userPaths = @("C:\Users\ci.migrate`$\.aws\credentials", "C:\Users\jumpbox.jenkins\.aws\credentials") + foreach ($path in $userPaths) { + if (-not (Test-Path -Path $path)) { + continue + } + try { + $nextLine = $false + (Get-Content -Path $path) | ForEach-Object { + if ($nextLine) { + Write-Host "$($env:computername) : $_" + $nextLine = $false + return + } else { + if ($_ -eq $sb_role) { + $nextLine = $true + } + } + } + } catch {} + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AWSConfigEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AWSConfigEntries.ps1 new file mode 100644 index 0000000..98295ae --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AWSConfigEntries.ps1 @@ -0,0 +1,52 @@ +function Get-AWSConfigEntries { +<# +.SYNOPSIS + Used to read/parse the AWS config entries to a queryable format +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $awsConfigPath = (Expand-Path "~/.aws/config") + if (!(Test-Path $awsConfigPath)) { + Write-Error "$logLead : Can not get path, are you sure you've setup your profile for AWS? Maybe run {function:Initialize-AWSCredentials}" + return + } + + $objects = @() + + $lines = (Get-Content $awsConfigPath) + + $profile = @{} + foreach ($line in $lines) { + if ($line.StartsWith('#')) { + # this is a comment, ignore it + } elseif ($line.StartsWith('[')) { + # this is a new profile, push any old profile into the objects array and start fresh + if ($null -ne $profile.Name) { + # $profile object is populated, save it + $objects += $profile + } + $name = $line.Replace(']','').Split(' ')[1] + $profile = @{ Name = $name } + } elseif ([string]::IsNullOrWhiteSpace($line)) { + # ignore empty lines, natch + } else { + # the only thing left are valid properties to attach to an object + $splits = $line -split '=' + $name = $splits[0].Trim() + $value = $splits[1].Trim() + $profile.$name = $value + } + } + + # save the last value if it's populated (which it should be) + if ($null -ne $profile.Name) { + $objects += $profile + } + + return ($objects | ConvertTo-AWSConfigEntry | Sort-Object) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialEntries.ps1 new file mode 100644 index 0000000..7a4df24 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialEntries.ps1 @@ -0,0 +1,52 @@ +function Get-AWSCredentialEntries { +<# +.SYNOPSIS + Used to read/parse the AWS credential entries to a queryable format +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $awsCredentialPath = (Expand-Path "~/.aws/credentials") + if (!(Test-Path $awsCredentialPath)) { + Write-Error "$logLead : Can not get path, are you sure you've setup your profile for AWS? Maybe run {function:Initialize-AWSCredentials}" + return + } + + $objects = @() + + $lines = (Get-Content $awsCredentialPath) + + $profile = @{} + foreach ($line in $lines) { + if ($line.StartsWith('#')) { + # this is a comment, ignore it + } elseif ($line.StartsWith('[')) { + # this is a new profile, push any old profile into the objects array and start fresh + if ($null -ne $profile.Profile) { + # $profile object is populated, save it + $objects += $profile + } + $name = $line.Replace(']','').Replace('[','') + $profile = @{ Profile = $name } + } elseif ([string]::IsNullOrWhiteSpace($line)) { + # ignore empty lines, natch + } else { + # the only thing left are valid properties to attach to an object + $splits = $line -split '=' + $name = $splits[0].Trim() + $value = $splits[1].Trim() + $profile.$name = $value + } + } + + # save the last value if it's populated (which it should be) + if ($null -ne $profile.Profile) { + $objects += $profile + } + + return ($objects | ConvertTo-AWSCredentialEntry | Sort-Object) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialSetter.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialSetter.ps1 new file mode 100644 index 0000000..46cd0c7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AWSCredentialSetter.ps1 @@ -0,0 +1,7 @@ +function Get-AWSCredentialSetter { +<# +.SYNOPSIS + Mostly only useful for people who routinely swap between machines or VM and host and wanna move their most recent AWS creds to another machine +#> + "Set-Content -Value '$((get-content ~/.aws/credentials) -join "','")' -Path ~/.aws/credentials" | Set-Clipboard +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AWSRegions.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AWSRegions.ps1 new file mode 100644 index 0000000..7a800f6 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AWSRegions.ps1 @@ -0,0 +1,8 @@ +function Get-AWSRegions { + return @( + 'us-east-1' + 'us-east-2' + 'us-west-1' + 'us-west-2' + ) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllAppSettings.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AllAppSettings.ps1 new file mode 100644 index 0000000..01560ae --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllAppSettings.ps1 @@ -0,0 +1,69 @@ +function Get-AllAppSettings { + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $false)] + [Alias("Path")] + [string[]]$FilePaths = @(), + + [Parameter(Mandatory = $false)] + [Alias("Computer")] + [string]$ComputerName = "localhost", + + [Parameter(Mandatory = $false)] + [Alias('Key')] + [Alias('Name')] + [string]$KeyName + ) + + $logLead = (Get-LogLeadName) + + $normalizedPaths = @() + + if (!(Any $FilePaths)) { + $FilePaths = @() + $FilePaths += (Get-DotNetConfigPath -use64Bit $true) + $FilePaths += (Get-DotNetConfigPath -use64Bit $false) + } + + foreach($path in $FilePaths) { + $normalizedPaths += (Get-NormalizedPath -FilePath $path -ComputerName $ComputerName) + } + + $returnValues = @() + + foreach($path in $normalizedPaths) { + if (!(Test-Path $path)) { + Write-Warning "$logLead : Could not connect to [$path], continuing" + continue + } + $content = (Get-Content $path -Raw) + $content0 = $content[0] + if ([string]::IsNullOrWhiteSpace($content0)) { + Write-Warning "$logLead : No content found in [$path], continuing" + continue + } + if ($content0 -eq '<') { + $content = ([xml]$content).configuration.appSettings.add + + foreach($appSetting in $content) { + if ([string]::IsNullOrWhiteSpace($KeyName) -or (![string]::IsNullOrWhiteSpace($KeyName) -and ($appSetting.Key -match $KeyName))) { + $returnValues += @{ Key = $appSetting.Key; Value = $appSetting.Value; Path = $path; } + } + } + } elseif (@('[','{') -contains $content0) { + $content = (Get-Json -JsonBlob $content) + $appSettings = Get-FlattenedAppsettingJson $content + foreach($appSetting in $appSettings) { + if ([string]::IsNullOrWhiteSpace($KeyName) -or (![string]::IsNullOrWhiteSpace($KeyName) -and ($appSetting.Key -match $KeyName))) { + $returnValues += @{ Key = $appSetting.Key; Value = $appSetting.Value; Path = $path; } + } + } + } else { + Write-Warning "$logLead : Do not know how to process [$path] for character-0 of [$content0], continuing" + continue + } + } + + return $returnValues +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllInstalledComponentsByType.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AllInstalledComponentsByType.ps1 new file mode 100644 index 0000000..3ff2279 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllInstalledComponentsByType.ps1 @@ -0,0 +1,67 @@ +function Get-AllInstalledComponentsByType { +<# +.SYNOPSIS + Get all the packages with manifests on this server which have the specified ComponentType + +.PARAMETER ComparableComponentType + The type of component to compare against +#> + param ( + [Parameter(Mandatory=$true)] + [ValidateSet("Widget","Provider","WebExtension","WebApplication","Service","Hotfix")] + [Alias('ComponentType')] + [string]$ComparableComponentType + ) + + $logLead = (Get-LogLeadName) + + # Due to a quirk, Service was keyed as Alkami.Installer.Services and not caught early enough + # This is considered acceptable because migrations also need a secondary path option for this routing + + $RoutedComponentType = $ComparableComponentType + + if ($ComparableComponentType -eq "Service") { + $RoutedComponentType = "Services" + } + + $chocoPath = (Get-ChocolateyInstallPath) + + $manifestLocations = @() + + $manifestLocations += (Get-ChildItem -Path $chocoPath -Recurse -Filter AlkamiManifest.xml) + + $manifestLocations += (Get-ChildItem -Path $chocoPath -Recurse -Filter AlkamiManifest.json) + + $manifestLocations += (Get-ChildItem -Path $chocoPath -Recurse -Filter AlkamiManifest.y?ml) + + $allPackages = @() + + foreach($manifestLocation in $manifestLocations) { + $componentType = (Get-PackageManifest -Path $manifestLocation.FullName).general.componentType + if ($componentType -eq $RoutedComponentType) { + $packageName = (Get-ChocoPackageFromPath $manifestLocation.FullName) + $allPackages += (New-Object -TypeName PSObject -Property @{ PackageName = $packageName; ManifestPath = $manifestLocation.FullName; }) + } + } + + # Remove (SDK components + widgets) /src folders because those also contain a secondary manifest underneath + $packageMap = @{} + foreach ($package in $allPackages) { + # Two conditions: + # The value in the map is currently null, so we store it + # The value we already have in the map is _longer_ than the one we are staring at, so let's replace it with the shorter path. + # Shorter paths are the paths in the roots, which is what we want. + # re: -gt => If they are the same length, then it's probably the same file, which idk, it's fine + if (($null -eq $packageMap[$package.PackageName]) -or ($packageMap[$package.PackageName].Length -gt $package.ManifestPath.Length)) { + $packageMap[$package.PackageName] = $package.ManifestPath + } + } + + # Now rebuild the allPackages list + $allPackages = @() + foreach ($key in $packageMap.Keys) { + $allPackages += (New-Object -TypeName PSObject -Property @{ PackageName = $key; ManifestPath = $packageMap[$key]; }) + } + + return $allPackages +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllRunningServicesForEnvironment.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AllRunningServicesForEnvironment.ps1 new file mode 100644 index 0000000..3e6ac39 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllRunningServicesForEnvironment.ps1 @@ -0,0 +1,96 @@ +function Get-AllRunningServicesForEnvironment { +<# +.SYNOPSIS + Used to get the list of all running services for an environment by SubscriptionService registration. + If no computer name is provided, defaults to the current server for configuration lead derivation. + +.PARAMETER ComputerName + Used to specify a computer name to look at for the subscription service + +.PARAMETER FilterToSpecifiedComputerName + Used to specify to filter to the provided computer name, or the current computer name if not provided on ComputerName +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + $ComputerName = 'local.dev.alkamitech.com', + [switch]$FilterToSpecifiedComputerName + ) + + $logLead = (Get-LogLeadName) + + $appSettingSplat = @{ + Key = "SubscriptionServiceMachine"; + } + + $magicString = "local.dev.alkamitech.com" + $magicStringProvided + + $magicStringProvided = ($ComputerName -eq $magicString) + + if ($magicStringProvided) { + $SubscriptionServiceMachine = $ComputerName + } else { + if (![string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Host "$logLead : Using [$ComputerName] as filter parameter" + $appSettingSplat.ComputerName = $ComputerName + } + + $SubscriptionServiceMachine = (Get-AppSetting @appSettingSplat) + Write-Host "$logLead : Got subscription host setting of [$SubscriptionServiceMachine]" + # Got either localhost or an empty string but was not given a hostname to start with, so default to the local machine. + # If we were given a hostname originally and our configuration value returned an empty string or localhost, let's just try to use that original value. + if (Compare-StringToLocalMachineIdentifiers $SubscriptionServiceMachine) { + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + Write-Host "$logLead : Using local computername for subscription host value" + $SubscriptionServiceMachine = (Get-FullyQualifiedServerName) + } else { + Write-Host "$logLead : fetching remote fully qualified computer name from provided computer name" + $SubscriptionServiceMachine = [System.Net.Dns]::Resolve($ComputerName).HostName + if ([string]::IsNullOrWhiteSpace($SubscriptionServiceMachine)) { + Write-Host "$logLead : fetching remote fully qualified computer name from provided computer name with Invoke-Command fallback, if possible" + $SubscriptionServiceMachine = (Invoke-Command -ComputerName $ComputerName -ScriptBlock { return (Get-FullyQualifiedServerName) }) + } + } + Write-Host "$logLead : Decided to use [$SubscriptionServiceMachine] as the subscription host value" + } + } + + $response = "" + + try { + $response = Invoke-RestMethod -Uri (New-Object UriBuilder "https",$SubscriptionServiceMachine, 50003, "services").Uri + } catch { + Write-Error "$logLead : Could not get the list of services. Are you pointed at the correct computer to test for? Is SubscriptionService running in that environment on that server?" + } + + $services = @() + $returnServices = @() + try { + $services = @((ConvertFrom-Json $response).Services) + $returnServices = $services + } catch { + Write-Error "$logLead : Could not parse the response from the webrequest as valid json. Use VerbosePreference to see full result in Write-Verbose stream" + Write-Verbose $response + } + + if (Test-IsCollectionNullOrEmpty $services) { + Write-Warning "$logLead : no services returned from remote server, or object not parsed as such. Use VerbosePreference to see full result in Write-Verbose stream" + Write-Verbose $response + return $null # can't do anything here + } + + if ($FilterToSpecifiedComputerName) { + if ([string]::IsNullOrWhiteSpace($ComputerName)) { + $ComputerName = $env:computername + } + $returnServices = $services.Where({$_.host -match $ComputerName}) + + if (Test-IsCollectionNullOrEmpty $returnServices) { + Write-Warning "$logLead : no services found that match filter, but otherwise results returned. Returning all results (for further investigation)" + $returnServices = $services + } + } + + return $returnServices +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1 new file mode 100644 index 0000000..efb4b98 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1 @@ -0,0 +1,198 @@ +function Get-AllServices { +<# +.SYNOPSIS + Used to find all services on a system but with all the details that we might use + Queries the registry to find all services that have at least an ImagePath, but filters out a majority of Windows Services (not all) + +.PARAMETER All + Returns everything that wasn't filtered out for other reasons + +.PARAMETER Name + Used to match names + +.PARAMETER Path + Used to match paths + +.PARAMETER Fragment + Used to search for any occurrence on path or name + +.PARAMETER Exact + Used to specify matching an exact path or name +#> + [CmdletBinding(DefaultParameterSetName = 'All')] + param ( + [Parameter(ParameterSetName = 'All')] + [switch]$All, + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [Alias('NameLike')] + [Alias('NamePartial')] + [Alias('NameFragment')] + [string]$Name, + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + [Alias('PathLike')] + [Alias('PathPartial')] + [Alias('PathFragment')] + [string]$Path, + [Parameter(Mandatory = $true, ParameterSetName = 'Fragment')] + [Alias('Query')] + [string]$Fragment, + [Parameter(Mandatory = $false, ParameterSetName = 'Name')] + [Parameter(Mandatory = $false, ParameterSetName = 'Path')] + [switch]$Exact, + [switch]$RefreshCache + ) + begin { + $logLead = (Get-LogLeadName) + + # Define the defaults for the ServiceController response to add some custom display information + $defaultTypeName = 'ServiceObject' + $defaultKeys = @('Name') + $defaultDisplaySet = @('Name', 'RunAs', 'StartMode', 'ExePath') + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet',[string[]]$defaultKeys) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet,$defaultKeyPropertySet) + } + process { + $contains = $Name.IndexOf('*') -or $Path.IndexOf('*') -or $Fragment.IndexOf('*') + if ($contains -and $Exact) { + throw "$logLead : Can not search for an Exact match AND use wildcards" + } + $checkPath = ![string]::IsNullOrWhiteSpace($Path) + $checkName = ![string]::IsNullOrWhiteSpace($Name) + if (![string]::IsNullOrWhiteSpace($Fragment)) { + $checkPath = $true + $checkName = $true + $Name = $Fragment + $Path = $Fragment + } + + $Name = $Name -Replace '\*','' + $Path = $Path -Replace '\*','' + + if ($RefreshCache) { + $global:get_allservices_servicesWithPaths = $null + } + + if ($null -ne $global:get_allservices_servicesWithPaths) { + $servicesWithPaths = $global:get_allservices_servicesWithPaths + } else { + $allServices = Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Services\ + + # If there is no path property, we really don't care for the purposes of this function + $servicesWithAnyPath = $allServices.Where({$_.Property -Contains 'ImagePath'}) + $servicesWithPaths = $servicesWithAnyPath.Where({!$_.GetValue('ImagePath').StartsWith("\SystemRoot")}) + $global:get_allservices_servicesWithPaths = $servicesWithPaths + } + + # https://docs.microsoft.com/en-us/windows/win32/msi/serviceinstall-table + $serviceType = @{0x1 = 'KernelDriver';0x2 = 'FileSystemDriver';0x4 = 'Adapter';0x8 = 'RecognizerDriver';0x10 = 'Win32OwnProcess';0x20 = 'Win32ShareProcess';0x100 = 'InteractiveProcess';} + $startMode = ('Boot','System','Automatic','Manual','Disabled') + $errorControl = ('Ignore','Normal','Severe','Critical') + + $services = @() + foreach ($regService in $servicesWithPaths) { + $serviceName = (Split-Path -Path $regService -Leaf) + + if ($checkName) { + $checkMatch = $checkName -and ($serviceName.IndexOf($Name, [StringComparison]::OrdinalIgnoreCase) -gt -1) + if ($checkName) { + # Write-Host "$logLead : $checkMatch | $serviceName | $Name" + } + $exactMatch = $Exact -and ($serviceName -eq $Name) + if ($Exact) { + # Write-Host "$logLead : $exactMatch | $serviceName | $Name" + } + + if ($checkMatch -or $exactMatch) { + # We can continue, we found a matching name + # Write-Host "$logLead : Found $serviceName matched" + } else { + continue + } + } + + $displayName = $regService.GetValue("DisplayName") + if (![string]::IsNullOrWhiteSpace($displayName)) { if ($displayName.IndexOf('@') -eq 0) { + if ($displayName.IndexOf(';') -gt -1) { + $displayName = $displayName.Substring($displayName.LastIndexOf(';')) + } + }} else { + $displayName = $serviceName + } + if ($displayName.IndexOf('@') -eq 0) { + # Neat trick: Alkami doesn't care about these services _at all_ + continue + } + + $description = $regService.GetValue("Description") + if (![string]::IsNullOrWhiteSpace($description)) { if ($description.IndexOf('@') -eq 0) { + # Neat trick: Alkami _also_ doesn't care about these services _at all_ + continue + }} + + $imagePath = $regService.GetValue("ImagePath") + $exePath = $imagePath + if (![string]::IsNullOrWhiteSpace($imagePath)) { if ($imagePath.IndexOf('"') -gt -1) { + $exePath = ($imagePath.Remove($imagePath.LastIndexOf(".exe",[StringComparison]::OrdinalIgnoreCase)) + ".exe").Replace('"','') + }} + $loginName = ($regService.GetValue("ObjectName"),'LocalSystem' -ne $null)[0] + + if ($checkPath) { + $checkMatch = $checkPath -and ($exePath.IndexOf($Path, [StringComparison]::OrdinalIgnoreCase) -gt -1) + if ($checkPath) { + # Write-Host "$logLead : $checkMatch | $exePath | $Path" + } + $exactMatch = $Exact -and ($exePath -eq $Path) + if ($Exact) { + # Write-Host "$logLead : $exactMatch | $exePath | $Path" + } + + if ($checkMatch -or $exactMatch) { + # We can continue, we found a matching path + # Write-Host "$logLead : Found $exePath matched" + } else { + continue + } + } + + $startModeValue = $regService.GetValue("Start") + + $obj = @{ + Name = $serviceName + StartMode = $startMode[$startModeValue] + StartType = <#[System.ServiceProcess.ServiceStartMode]#>$startModeValue + ErrorControl = $errorControl[$regService.GetValue("ErrorControl")] + ImagePath = $imagePath + ExePath = $exePath + RunAs = $loginName + DisplayName = $displayName + Description = $description + Dependencies = $regService.GetValue("Dependencies") + ManagedServiceAccount = [System.BitConverter]::ToInt32(($regService.GetValue("ServiceAccountManaged"),@(0,0,0,0) -ne $null)[0],0) -eq 1 + FailureActions = ConvertFrom-FailureActions -FailureActions $regService.GetValue("FailureActions") + # FailureActionsOnNonCrashFailures https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actions_flag + # If this member is TRUE and the service has configured failure actions, the failure actions are queued if the service process terminates without reporting a status of SERVICE_STOPPED or if it enters the SERVICE_STOPPED state but the dwWin32ExitCode member of the SERVICE_STATUS structure is not ERROR_SUCCESS (0). + # If this member is FALSE and the service has configured failure actions, the failure actions are queued only if the service terminates without reporting a status of SERVICE_STOPPED. + FailureActionsOnNonCrashFailures = $regService.GetValue("FailureActionsOnNonCrashFailures") -eq 1 + FailureCommand = $regService.GetValue("FailureCommand") + DelayedAutostart = $regService.GetValue("DelayedAutostart") + AutoRun = $regService.GetValue("AutoRun") + AutoRunAlwaysDisable = $regService.GetValue("AutoRunAlwaysDisable") + AutorunsDisabled = $regService.GetValue("AutorunsDisabled") + StateFlags = $regService.GetValue("StateFlags") + Type = $serviceType[$regService.GetValue("Type")] + # Where we find this key + # Key = $regService + # The properties this object has so we can get the data later for confirming we got everything we want + # Property = $regService.Property + } + + $serviceController = New-Object PSCustomObject -Property $obj + $serviceController.PSObject.TypeNames.Insert(0,$defaultTypeName) + $serviceController | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers + + $services += $serviceController + } + return $services + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1xml b/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1xml new file mode 100644 index 0000000..53784c4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllServices.ps1xml @@ -0,0 +1,54 @@ + + + + + ServiceObjects + + ServiceObject + + + + + + DefaultServiceObjectView + + ServiceObjects + + + + + + + + + + + + + + + + + + + + + + Name + + + RunAs + + + StartMode + + + ExePath + + + + + + + + \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AllUsersNotLoggedInSince.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AllUsersNotLoggedInSince.ps1 new file mode 100644 index 0000000..00bc5a3 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AllUsersNotLoggedInSince.ps1 @@ -0,0 +1,50 @@ +function Get-AllUsersNotLoggedInSince { + [CmdLetBinding()] + [OutputType([object[]])] + param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [DateTime]$LastLoginDate = [DateTime]::Now.AddMonths(-3) + ) + $userLookup = @() + $domainControllerLookup = @{} + + # aka not a system account, like LOCALSYSTEM or NETWORKSERVICE + $isUser = 1 + # aka not IIS services + $passwordCannotChange = 64 + # aka not gMSA + $workstationTrustAccount = 4096 + # aka not local accounts for things like machine recovery + $passwordDoesNotExpire = 65536 + + $allLoginProfiles = Get-CimInstance -ClassName Win32_NetworkLoginProfile + $users = $allLoginProfiles.Where({ ($_.Flags -band $isUser) -and -not ($_.Flags -band $passwordCannotChange) -and -not ($_.Flags -band $workstationTrustAccount) -and -not ($_.Flags -band $passwordDoesNotExpire) }) + + foreach ($user in $users) { + $domain = ($user.Name -split '\\')[0] + $username = ($user.Name -split '\\')[1] + $server = $domainControllerLookup.$domain + if ($null -eq $server) { + $server = (Get-ADDomainController -Discover -DomainName $domain).Hostname[0] + $domainControllerLookup.$domain = $server + } + $domainUser = Get-ADUser -Server $server -Identity $username + if ($null -ne $domainUser) { + # calculate directory size + $homeDirectoryPath = Join-Path -Path C:\Users\ -ChildPath $username + $sizeInMbs = [System.Math]::Round( ((Get-ChildItem -Path $homeDirectoryPath -Recurse -ErrorAction SilentlyContinue -Force) | Measure-Object -Property Length -Sum).Sum / 1Mb, 2) + if ($user.LastLogon -lt $LastLoginDate) { + $userLookup += @{ + Username = $user.Name + LastLogon = $user.LastLogon + HomeFolderMB = $sizeInMbs + } + } + } else { + Write-Host "Could not find $($user.Caption) in $domain" + } + } + + return $userLookup +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-AwsStandardDynamicParameters.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-AwsStandardDynamicParameters.ps1 new file mode 100644 index 0000000..52962ae --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-AwsStandardDynamicParameters.ps1 @@ -0,0 +1,41 @@ +function Get-AwsStandardDynamicParameters { + + [Cmdletbinding()] + param( + + [Parameter(Mandatory=$false)] + [string]$RegionParameterName = "Region", + + [Parameter(Mandatory=$false)] + [switch]$RegionParameterRequired, + + [Parameter(Mandatory=$false)] + [string]$RegionParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory=$false)] + [string]$ProfileParameterName = "Profile", + + [Parameter(Mandatory=$false)] + [switch]$ProfileParameterRequired, + + [Parameter(Mandatory=$false)] + [string]$ProfileParameterSetName = "__AllParameterSets" + ) + + $runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + $regionRuntimeParameter = Get-DynamicAwsRegionParameter -GeneratedParameterName $RegionParameterName ` + -ParameterSetName $RegionParameterSetName ` + -IsMandatoryParameter:$RegionParameterRequired + + $profileRuntimeParameter = Get-DynamicAwsProfilesParameter -GeneratedParameterName $ProfileParameterName ` + -ParameterSetName $ProfileParameterSetName ` + -IsMandatoryParameter:$ProfileParameterRequired + + # The Dynamic params functions return dictionaries so they can be used independently if needed + # We will add both returned values to a new Dictionary and return it for use by the calling function + $runtimeParameterDictionary.Add($RegionParameterName, $($regionRuntimeParameter.Values[0])) + $runtimeParameterDictionary.Add($ProfileParameterName, $($profileRuntimeParameter.Values[0])) + + return $runtimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-BasicAuthWebHeader.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-BasicAuthWebHeader.ps1 new file mode 100644 index 0000000..c0db354 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-BasicAuthWebHeader.ps1 @@ -0,0 +1,22 @@ +function Get-BasicAuthWebHeader { + [CmdletBinding()] + [OutputType([object])] + param( + [Parameter(Mandatory = $true, ParameterSetName = "UsernameAndPassword")] + [string]$Username, + [Parameter(Mandatory = $true, ParameterSetName = "UsernameAndPassword")] + [Alias('Password')] + [string]$InputObject, + [Parameter(Mandatory = $true, ParameterSetName = "Credential")] + [System.Management.Automation.PSCredential]$Credential + ) + + if ($PSCmdlet.ParameterSetName -eq "Credential") { + $InputObject = Get-PasswordFromCredential -Credential $Credential + $Username = $Credential.Username + } + + $base64String = (ConvertTo-Base64 -Input "$($Username):$($InputObject)") + + return @{ Authorization = "Basic $base64String" } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-BitlockerDriveInformation.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-BitlockerDriveInformation.ps1 new file mode 100644 index 0000000..c1510f9 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-BitlockerDriveInformation.ps1 @@ -0,0 +1,60 @@ +Function Get-BitlockerDriveInformation { +<# +.SYNOPSIS + This function only requires that you are a local administrator, and can tell the decryption information for your BitLocker drives (where available). +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-IsUserLocalAdministrator)) { + Write-Warning "$logLead : You do not appear to be an administrator on this machine. Information can not be retrieved." + Write-Warning "$logLead : Did you mean to run this with elevated privileges?" + return + } + + if ($null -eq (Get-Command Manage-BDE)) { + Write-Warning "$logLead : No utilities found to manage BitLocker Device Encryption (missing Manage-BDE). Can not continue." + return + } + + $driveRoots = (Get-LocalHardDriveRoots) + + $return = @() + + foreach ($root in $driveRoots) { + $text = (Manage-BDE -Protectors $root -Get -Type RecoveryPassword) + $result = @{ DriveLetter = $root; } + $foundBlock = $false + $foundPassword = $false + $foundError = $false + foreach($line in $text) { + if ($line.Trim().StartsWith("Numerical Password:")) { + $foundBlock = $true + } elseif (($foundBlock -eq $true) -and ($line.Trim().StartsWith("Password:"))) { + $foundPassword = $true + } elseif ($foundPassword -eq $true) { + $result.Password = $line.Trim() + $result.Status = "Password Retrieved" + $foundPassword = $false + } elseif ($line.Trim().StartsWith("ERROR:")) { + $foundError = $true + } elseif ($foundError -eq $true) { + $result.Error = $line.Trim() + $result.Status = "Error Occurred" + } else { + Write-Debug "$logLead : Discarded line: $root - $line" + } + } + if (([string]::IsNullOrWhiteSpace($result.Password)) -and ($foundError -eq $false)) { + $result.Result = $text + $result.Status = "Results indeterminate. Review Result block for more details." + } + $return += $result + } + + return $return +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CacheFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CacheFile.ps1 new file mode 100644 index 0000000..946bb5e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CacheFile.ps1 @@ -0,0 +1,25 @@ +function Get-CacheFile { + param ( + [Parameter(Mandatory = $true, Position = 0)] + [Alias('CacheKey')] + [Alias('FileName')] + $Name + ) + + $defaultCacheFolderName = "CachedFiles" + $alkamiPDPath = (Join-Path -Path $env:ProgramData -ChildPath "Alkami") + $defaultCacheFolder = (Join-Path $alkamiPDPath $defaultCacheFolderName) + + $cacheFolder = (Get-EnvironmentVariable -Name "COLEMODULE_$($defaultCacheFolderName)_Path" 6>$null 5>$null 4>$null 3>$null) + if ([string]::IsNullOrWhiteSpace($overriddenCacheFolder)) { + $cacheFolder = $defaultCacheFolder + } + + $targetPath = (Join-Path $cacheFolder $Name) + + if (!(Test-Path $cacheFolder)) { + New-Item -Path $cacheFolder -ItemType Directory -Force + } + + return $targetPath +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CachePathDesignations.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathDesignations.ps1 new file mode 100644 index 0000000..7d72e63 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathDesignations.ps1 @@ -0,0 +1,21 @@ +function Get-CachePathDesignations { +<# +.SYNOPSIS + Get the path for the cached designation data + +.PARAMETER ProfileName + A valid ProfileName +#> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$ProfileName + ) + + Assert-ValidAWSProfileName -ProfileName $ProfileName + + $ProfileName = $ProfileName.Replace("temp-","").ToLower() + $cacheFile = Get-CacheFile -Name "Designations.$ProfileName.json" + return $cacheFile +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CachePathEC2Instance.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathEC2Instance.ps1 new file mode 100644 index 0000000..383f880 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathEC2Instance.ps1 @@ -0,0 +1,21 @@ +function Get-CachePathEC2Instance { +<# +.SYNOPSIS + Get the path for the cached EC2Instance data + +.PARAMETER ProfileName + A valid ProfileName +#> + [CmdletBinding()] + [OutputType([object[]])] + param( + [Parameter(Mandatory = $true)] + [string]$ProfileName + ) + + Assert-ValidAWSProfileName -ProfileName $ProfileName + + $ProfileName = $ProfileName.Replace("temp-","").ToLower() + $cacheFile = Get-CacheFile -Name "EC2Instances.$ProfileName.json" + return $cacheFile +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CachePathJiraTeams.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathJiraTeams.ps1 new file mode 100644 index 0000000..aaaa625 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CachePathJiraTeams.ps1 @@ -0,0 +1,13 @@ +function Get-CachePathJiraTeams { +<# +.SYNOPSIS + Get the path for the cached Jira Teams data +#> + [CmdletBinding()] + [OutputType([object[]])] + param( + ) + + $cacheFile = Get-CacheFile -Name "JiraTeams.json" + return $cacheFile +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CachedInstances.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CachedInstances.ps1 new file mode 100644 index 0000000..8195b7e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CachedInstances.ps1 @@ -0,0 +1,99 @@ +function Get-CachedInstances { +<# +.SYNOPSIS + Get the cached instance data. Defaults to dev data + +.PARAMETER ProfileName + A valid ProfileName +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $false)] + [string]$ProfileName = (Get-LocalCachedAWSProfile), + [Parameter(Mandatory = $false)] + [ValidateScript({ + $ProfileName = Get-Coalesce $PSCmdlet.MyInvocation.BoundParameters["ProfileName"], (Get-LocalCachedAWSProfile) + (Get-Designations -ProfileName $ProfileName) -contains $_ + })] + [string]$Designation, + [Parameter()] + [switch]$MIC, + [Parameter()] + [switch]$WEB, + [Parameter()] + [switch]$APP, + [Parameter()] + [switch]$TeamCity, + [Parameter()] + [switch]$IncludeDR, + [Parameter()] + [switch]$OnlyDR + ) + + $logLead = (Get-LogLeadName) + + # if no server types were selected, then we want all server types + $noServerTypesSelected = $false + if (-not $WEB -and -not $MIC -and -not $APP -and -not $TeamCity) { + $noServerTypesSelected = $true + } + # true up selection as required + $WEB = $WEB -or $noServerTypesSelected + $MIC = $MIC -or $noServerTypesSelected + $APP = $APP -or $noServerTypesSelected + $TeamCity = $TeamCity -or $noServerTypesSelected + + if ($OnlyDR -and $IncludeDR) { + Write-Warning "$logLead : You can not select OnlyDR and IncludeDR. Opting for IncludeDR." + $OnlyDR = $false + } + + $designationSpecified = -not [string]::IsNullOrWhiteSpace($Designation) + + Assert-ValidAWSProfileName -ProfileName $ProfileName + + $cachePath = Get-CachePathEC2Instance -ProfileName $ProfileName + + if (!(Test-Path -Path $cachePath)) { + Write-Warning "$logLead : No file found at [$cachePath]. You probably should call Checkpoint-EC2Instances -ProfileName $ProfileName" + return @() + } + + if (Test-WasFileModifiedWithin -Path $cachePath -Last Week) { + if (Test-IsCurrentAWSUserSessionValid -ProfileName $ProfileName) { + Write-Information "$logLead : Instance information is more than 7 days out of date, and you have a valid session token. Updating EC2 instance information." + Checkpoint-EC2Instances -ProfileName $ProfileName + } else { + Write-Warning "$logLead : Your $ProfileName designations file is more than 7 days out of date. Do you need to run [Checkpoint-EC2Instances -ProfileName $ProfileName] again?" + } + } + + $instances = Get-Json -Path $cachePath | Where-Object { + $instance = $_ + $result = $false + if ($WEB) { $result = $result -or "$($instance.Hostname)".ToLower().StartsWith('web') } + if ($MIC) { $result = $result -or "$($instance.Hostname)".ToLower().StartsWith('mic') } + if ($APP) { $result = $result -or "$($instance.Hostname)".ToLower().StartsWith('app') } + if ($TeamCity) { $result = $result -or "$($instance.Hostname)".ToLower().StartsWith('tea') } + if ($noServerTypesSelected) { $result = $true } + $result + } | Where-Object { + $instance = $_ + if ($OnlyDR) { + $instance.tags."alk:env" -eq "dr" + } else { + if ($IncludeDR) { + $true + } else { + $instance.tags."alk:env" -ne "dr" + } + } + } | Where-Object { + $instance = $_ + if ($designationSpecified) { $instance.Designation -eq $Designation } + else { $true } + } + + return ($instances | ConvertTo-Instance) +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-Callstack.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-Callstack.ps1 new file mode 100644 index 0000000..3b06aa3 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-Callstack.ps1 @@ -0,0 +1,12 @@ +function Get-Callstack { + [CmdletBinding()] + param() + + $TraceAction = Trace-ActionStart -ActionName "Get Callstack" + + $return = (Get-PSCallstack) + + Trace-ActionEnd -TraceAction $TraceAction + + return $return +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CallstackParentFunctionNames.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CallstackParentFunctionNames.ps1 new file mode 100644 index 0000000..0015750 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CallstackParentFunctionNames.ps1 @@ -0,0 +1,11 @@ +function Get-CallstackParentFunctionNames { +<# +.SYNOPSIS + Get the name of all the functions that got us to here +#> + [CmdletBinding()] + [OutputType([System.String[]])] + param() + + return (Get-PSCallStack).Command +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CallstackWrapper.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CallstackWrapper.ps1 new file mode 100644 index 0000000..4d02053 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CallstackWrapper.ps1 @@ -0,0 +1,12 @@ +function Get-CallstackWrapper { + [CmdletBinding()] + param() + + $TraceAction = Trace-ActionStart -ActionName "Get Callstack" + + $return = (Get-Callstack) + + Trace-ActionEnd -TraceAction $TraceAction + + return $return +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ChatFunkyString.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ChatFunkyString.ps1 new file mode 100644 index 0000000..91281fb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ChatFunkyString.ps1 @@ -0,0 +1,96 @@ +Function Get-ChatFunkyString { + param ( + [string]$str + ) + + if ([string]::IsNullOrWhiteSpace($str)) { + Write-Host "Can't funkitize an empty string" + return + } + + $replacements = @{ + "0" = ":x01:" + "1" = ":x11:" + "2" = ":x21:" + "3" = ":x31:" + "4" = ":x41:" + "5" = ":x51:" + "6" = ":x61:" + "7" = ":x71:" + "8" = ":x81:" + "9" = ":x91:" + "a" = ":xa1:" + "&" = ":xampersand1:" + "'" = ":xapostrophe1:" + "*" = ":xasterisk1:" + "@" = ":xatsymbol1:" + "b" = ":xb1:" + "\" = ":xbackslash1:" + "c" = ":xc1:" + "^" = ":xcarat1:" + ":" = ":xcolon1:" + "," = ":xcomma1:" + "d" = ":xd1:" + "$" = ":xdollarsign1:" + "`"" = ":xdoublequote1:" + "e" = ":xe1:" + "=" = ":xequals1:" + "!" = ":xexclamation1:" + "f" = ":xf1:" + "/" = ":xforwardslash1:" + "g" = ":xg1:" + ">" = ":xgreaterthan1:" + "h" = ":xh1:" + "i" = ":xi1:" + "j" = ":xj1:" + "k" = ":xk1:" + "l" = ":xl1:" + "{" = ":xleftcurlybrace1:" + "(" = ":xleftparenthesis1:" + "[" = ":xleftsquarebracket1:" + "<" = ":xlessthan1:" + "m" = ":xm1:" + "-" = ":xminus1:" + "n" = ":xn1:" + "o" = ":xo1:" + "`#" = ":xoctothorpe1:" + "p" = ":xp1:" + "%" = ":xpercent1:" + "." = ":xperiod1:" + "|" = ":xpipe1:" + "+" = ":xplus1:" + "q" = ":xq1:" + "?" = ":xquestion1:" + "r" = ":xr1:" + "}" = ":xrightcurlybrace1:" + ")" = ":xrightparenthesis1:" + "]" = ":xrightsquarebracket1:" + "s" = ":xs1:" + ";" = ":xsemicolon1:" + " " = ":xspace1:" + "t" = ":xt1:" + "~" = ":xtilde1:" + "u" = ":xu1:" + "_" = ":xunderscore1:" + "v" = ":xv1:" + "w" = ":xw1:" + "x" = ":xx1:" + "y" = ":xy1:" + "z" = ":xz1:" + } + + $inArr = $str.ToCharArray() + $finalString = "" + + foreach($char in $inArr) { + $replacement = $replacements[([string]$char).ToLower()] + if ([string]::IsNullOrEmpty($replacement)) { + $replacement = $char + } + $finalString += $replacement + } + + Set-Clipboard $finalString + + Write-Host "the value is now on your clipboard" +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-Coalesce.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-Coalesce.ps1 new file mode 100644 index 0000000..85dd333 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-Coalesce.ps1 @@ -0,0 +1,28 @@ +function Get-Coalesce { +<# +.SYNOPSIS + Magic function that returns the first non-null argument passed in +#> + [OutputType([object])] + param ( + ) + + $logLead = (Get-LogLeadName) + + if (($args.Length -eq 1) -and ($args -is [System.Collections.IEnumerable] -and $args -isnot [string])) { + $args = $args[0] + } + + foreach ($arg in $args) { + if ($null -ne $arg) { + if (($arg -is [string]) -and [string]::IsNullOrWhiteSpace($arg)) { + #skip + } else { + return $arg + } + } + } + + Write-Verbose "$logLead : Could not find a non-null value, returning `$null." + return $null +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerInstallPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerInstallPath.ps1 new file mode 100644 index 0000000..207e9f7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerInstallPath.ps1 @@ -0,0 +1,30 @@ +function Get-ComponentInstallerInstallPath { + param( + [Parameter(Mandatory=$true)] + [ValidateSet("Widget","Provider","WebExtension","WebApplication","Service")] + [string]$ComponentType + ) + + $logLead = (Get-LogLeadName) + + # Due to a quirk, Service was keyed as Alkami.Installer.Services and not caught early enough + # This is considered acceptable because migrations also need a secondary path option for this routing + + $RoutedComponentType = $ComponentType + + if ($ComponentType -eq "Service") { + $RoutedComponentType = "Services" + } + + $alkamiPDPath = (Join-Path -Path $env:ProgramData -ChildPath "Alkami") + $installerPath = (Join-Path -Path $alkamiPDPath -ChildPath "Installer") + $targetInstallerPath = (Join-Path -Path $installerPath -ChildPath $RoutedComponentType) + + $finalPath = (Join-Path -Path $targetInstallerPath -ChildPath "install.ps1") + + if (!(Test-Path $finalPath)) { + Write-Warning "$logLead [$ComponentType] : Installer file does not exist. Have you tried [choco upgrade Alkami.Installer.$RoutedComponentType -y] lately?" + } + + return $finalPath +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerUninstallPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerUninstallPath.ps1 new file mode 100644 index 0000000..5644363 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ComponentInstallerUninstallPath.ps1 @@ -0,0 +1,30 @@ +function Get-ComponentInstallerUninstallPath { + param( + [Parameter(Mandatory=$true)] + [ValidateSet("Widget","Provider","WebExtension","WebApplication","Service")] + [string]$ComponentType + ) + + $logLead = (Get-LogLeadName) + + # Due to a quirk, Service was keyed as Alkami.Installer.Services and not caught early enough + # This is considered acceptable because migrations also need a secondary path option for this routing + + $RoutedComponentType = $ComponentType + + if ($ComponentType -eq "Service") { + $RoutedComponentType = "Services" + } + + $alkamiPDPath = (Join-Path -Path $env:ProgramData -ChildPath "Alkami") + $installerPath = (Join-Path -Path $alkamiPDPath -ChildPath "Installer") + $targetInstallerPath = (Join-Path -Path $installerPath -ChildPath $RoutedComponentType) + + $finalPath = (Join-Path -Path $targetInstallerPath -ChildPath "uninstall.ps1") + + if (!(Test-Path $finalPath)) { + Write-Warning "$logLead [$ComponentType] : Installer file does not exist. Have you tried [choco upgrade Alkami.Installer.$RoutedComponentType -y] lately?" + } + + return $finalPath +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ConfluenceBaseUrl.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ConfluenceBaseUrl.ps1 new file mode 100644 index 0000000..8e58c20 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ConfluenceBaseUrl.ps1 @@ -0,0 +1,22 @@ +function Get-ConfluenceBaseUrl { +<# +.SYNOPSIS + Return the base Confluence url. + May read the value from an environment variable if present +#> + [CmdletBinding()] + [OutputType([string])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $environmentVariable = (Get-EnvironmentVariable "CONFLUENCE_URL" 6>$null 5>$null 4>$null 3>$null) + if (![string]::IsNullOrWhiteSpace($environmentVariable)) { + Write-Information "$logLead : Returning environment variable" + return $environmentVariable + } + + Write-Information "$logLead : Returning default value" + return "https://confluence.alkami.com" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ConsoleDisplayWidth.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ConsoleDisplayWidth.ps1 new file mode 100644 index 0000000..d4fb3b9 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ConsoleDisplayWidth.ps1 @@ -0,0 +1,19 @@ +function Get-ConsoleDisplayWidth { +<# +.SYNOPSIS + Get the width of the current UI, if possible + +.PARAMETER DefaultDisplayWidth + Supply a value if you want the default to be considered something else +#> + param( + $DefaultDisplayWidth = 80 + ) + + $maxDisplayWidth = $DefaultDisplayWidth + if (($null -ne $host) -and ($null -ne $host.UI) -and ($null -ne $host.UI.RawUI) -and ($null -ne $host.UI.RawUI.WindowSize)) { + $maxDisplayWidth = $host.UI.RawUI.WindowSize.Width + } + + return $maxDisplayWidth +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CredentialFromEnvironmentVariables.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CredentialFromEnvironmentVariables.ps1 new file mode 100644 index 0000000..761d296 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CredentialFromEnvironmentVariables.ps1 @@ -0,0 +1,47 @@ +function Get-CredentialFromEnvironmentVariables { +<# +.SYNOPSIS + Get the user's credentials from the local environment variables + This is mostly useful as a Profile line such as: `$creds = (Get-CredentialFromEnvironmentVariables) + This way a developer can test faster with stored credentials without having to recreate them frequently +#> + param ( + ) + + $logLead = (Get-LogLeadName) + + $user = (Get-EnvironmentVariable -Name "CREDENTIAL_USERNAME" -Store User 6>$null 5>$null 4>$null 3>$null) + if ([string]::IsNullOrWhiteSpace($User)) { + Write-Warning "$logLead : Your cached username is out of sync with your configuration. Please update using Set-LocalUserCredential and then retry your task." + throw "$logLead : Username not present or corrupted" + } + + # handy decomposition magic trick + $userPartial = ($User -split '\\')[-1] + $PasswordLastSet,$PasswordNeverExpires,$PasswordExpired = (Get-ADUser -Filter "SamAccountName -eq '$userPartial'" -Properties PasswordLastSet, PasswordNeverExpires,PasswordExpired)['PasswordLastSet','PasswordNeverExpires','PasswordExpired'] + + if ($PasswordExpired) { + throw "$logLead : Your password is expired, you are gonna have a real bad day mate" + } + + if ([bool]$PasswordNeverExpires) { + # neat, but you probably shouldn't be using this account cached ... + } else { + $lastChangeDate = (Get-EnvironmentVariable -Name "CREDENTIAL_LASTCHANGED" -Store User 6>$null 5>$null 4>$null 3>$null) + $tempParseDate = [DateTime]::MinValue + if (![DateTime]::TryParse($lastChangeDate,[ref]$tempParseDate)) { + Write-Warning "$logLead : Your cached password record appears corrupted. Please update using Set-LocalUserCredential and then retry your task." + throw "$logLead : Stored password record appears corrupted" + } + + if ($PasswordLastSet -gt $tempParseDate) { + Write-Warning "$logLead : Your cached password is out of sync with your configuration. Please update using Set-LocalUserCredential and then retry your task." + throw "$logLead : Stored password appears to be out of sync" + } + } + + $secureStringPassword = (Get-EnvironmentVariable -Name "CREDENTIAL_PASSWORD" -Store User 6>$null 5>$null 4>$null 3>$null) + # If the password string is empty this just won't work so it'll throw on its own + + return New-Object System.Management.Automation.PSCredential -ArgumentList $user, (ConvertTo-SecureString $secureStringPassword) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CurrentDomainName.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CurrentDomainName.ps1 new file mode 100644 index 0000000..7a7b992 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CurrentDomainName.ps1 @@ -0,0 +1,15 @@ +function Get-CurrentDomainName { +<# +.SYNOPSIS + Get the current domain name of the machine +#> + [CmdletBinding()] + param ( + ) + + if (Test-IsWindowsPlatform) { + return (Get-CimInstance Win32_ComputerSystem -Property Domain).Domain + } + + return $null +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-CurrentUsername.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-CurrentUsername.ps1 new file mode 100644 index 0000000..2543066 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-CurrentUsername.ps1 @@ -0,0 +1,32 @@ +function Get-CurrentUsername { +<# +.SYNOPSIS + This function is just a shim to make it easier to figure out who you are +#> + [CmdletBinding()] + [OutputType([string])] + param() + + $logLead = (Get-LogLeadName) + + $currentUsername = $env:USERNAME + + if ([string]::IsNullOrWhiteSpace($currentUsername)) { + try { + $currentUsername = (whoami) + } catch { + Write-Verbose "$logLead : Is `whoami` supported on this system?" + } + } + + if ([string]::IsNullOrWhiteSpace($currentUsername)) { + # How is this empty? Maybe someone jacked with the environment variables? + $homePath = (Resolve-Path -Path ~\ -ErrorAction Ignore) + if ([string]::IsNullOrWhiteSpace($homePath)) { + throw "$logLead : I have no clue what the current username is. Fix this sometime when you can repro." + } + $currentUsername = (Split-Path -Path $homePath -Leaf) + } + + return $currentUsername +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-Designations.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-Designations.ps1 new file mode 100644 index 0000000..69aef46 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-Designations.ps1 @@ -0,0 +1,38 @@ +function Get-Designations { +<# +.SYNOPSIS + Get the list of designations for a given ProfileName + +.PARAMETER ProfileName + A valid ProfileName +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $false)] + [string]$ProfileName = (Get-LocalCachedAWSProfile) + ) + + $logLead = (Get-LogLeadName) + + Assert-ValidAWSProfileName -ProfileName $ProfileName + + $cachePath = Get-CachePathDesignations -ProfileName $ProfileName + + if (!(Test-Path -Path $cachePath)) { + Write-Warning "$logLead : No file found at [$cachePath]. You probably should call Checkpoint-EC2Instances -ProfileName $ProfileName" + return @() + } + + $item = Get-Item -Path $cachePath + if (Test-WasFileModifiedWithin -Path $cachePath -Last Week) { + if (Test-IsCurrentAWSUserSessionValid -ProfileName $ProfileName) { + Write-Information "$logLead : Instance information is more than 7 days out of date, and you have a valid session token. Updating EC2 instance information." + Checkpoint-EC2Instances -ProfileName $ProfileName + } else { + Write-Warning "$logLead : Your $ProfileName designations file is more than 7 days out of date. Do you need to run [Checkpoint-EC2Instances -ProfileName $ProfileName] again?" + } + } + + return (Get-Content $cachePath) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-DotNetProjectFileTypeFromGuid.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-DotNetProjectFileTypeFromGuid.ps1 new file mode 100644 index 0000000..f236b55 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-DotNetProjectFileTypeFromGuid.ps1 @@ -0,0 +1,73 @@ +function Get-DotNetProjectFileTypeFromGuid { +<# +.SYNOPSIS + Get the project type friendly name from the guid presented, if present + +.PARAMETER ProjectTypeGuid + [string] The project type guid to look up +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ProjectTypeGuid + ) + + return @{ + "06A35CCD-C46D-44D5-987B-CF40FF872267" = "Deployment Merge Module" + "14822709-B5A1-4724-98CA-57A101D1B079" = "Workflow (C#)" + "20D4826A-C6FA-45DB-90F4-C717570B9F32" = "Legacy (2003) Smart Device (C#)" + "2150E333-8FDC-42A3-9474-1A3956D46DE8" = "Solution Folder" + "2DF5C3F4-5A5F-47a9-8E94-23B4456F55E2" = "XNA (XBox)" + "32F31D43-81CC-4C15-9DE6-3FC5453562B6" = "Workflow Foundation" + "349C5851-65DF-11DA-9384-00065B846F21" = "Web Application (incl. MVC 5)" + "3AC096D0-A1C2-E12C-1390-A8335801FDAB" = "Test" + "3D9AD99F-2412-4246-B90B-4EAA41C64699" = "Windows Communication Foundation (WCF)" + "3EA9E505-35AC-4774-B492-AD1749C4943A" = "Deployment Cab" + "4D628B5B-2FBC-4AA6-8C16-197242AEB884" = "Smart Device (C#)" + "4F174C21-8C12-11D0-8340-0000F80270F8" = "Database (other project types)" + "54435603-DBB4-11D2-8724-00A0C9A8B90C" = "Visual Studio 2015 Installer Project Extension" + "593B0543-81F6-4436-BA1E-4747859CAAE2" = "SharePoint (C#)" + "603C0E0B-DB56-11DC-BE95-000D561079B0" = "ASP.NET MVC 1.0" + "60DC8134-EBA5-43B8-BCC9-BB4BC16C2548" = "Windows Presentation Foundation (WPF)" + "68B1623D-7FB9-47D8-8664-7ECEA3297D4F" = "Smart Device (VB.NET)" + "66A26720-8FB5-11D2-AA7E-00C04F688DDE" = "Project Folders" + "6BC8ED88-2882-458C-8E55-DFD12B67127B" = "MonoTouch" + "6D335F3A-9D43-41b4-9D22-F6F17C4BE596" = "XNA (Windows)" + "76F1466A-8B6D-4E39-A767-685A06062A39" = "Windows Phone 8/8.1 Blank/Hub/Webview App" + "786C830F-07A1-408B-BD7F-6EE04809D6DB" = "Portable Class Library" + "8BB2217D-0F2D-49D1-97BC-3654ED321F3B" = "ASP.NET 5" + "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942" = "C++" + "978C614F-708E-4E1A-B201-565925725DBA" = "Deployment Setup" + "9A19103F-16F7-4668-BE54-9A1E7A4F7556" = "SDK Style Project (ASP.NET Core, etc)" + "A1591282-1198-4647-A2B1-27E5FF5F6F3B" = "Silverlight" + "A5A43C5B-DE2A-4C0C-9213-0A381AF9435A" = "Universal Windows Class Library" + "A860303F-1F3F-4691-B57E-529FC101A107" = "Visual Studio Tools for Applications (VSTA)" + "A9ACE9BB-CECE-4E62-9AA4-C7E7C5BD2124" = "Database" + "AB322303-2255-48EF-A496-5904EB18DA55" = "Deployment Smart Device Cab" + "B69E3092-B931-443C-ABE7-7E7B65F2A37F" = "Micro Frmework" + "BAA0C2D2-18E2-41B9-852F-F413020CAA33" = "Visual Studio Tools for Office (VSTO)" + "BC8A1FFA-BEE3-4634-8014-F334798102B3" = "Windows Store Apps (Metro Apps)" + "BF6F8E12-879D-49E7-ADF0-5503146B24B8" = "C# in Dynamics 2012 AX AOT" + "C089C8C0-30E0-4E22-80C0-CE093F111A43" = "Windows Phone 8/8.1 App (C#)" + "C252FEB5-A946-4202-B1D4-9916A0590387" = "Visual Database Tools" + "CB4CE8C6-1BDB-4DC7-A4D3-65A1999772F8" = "Legacy (2003) Smart Device (VB.NET)" + "D399B71A-8929-442a-A9AC-8BEC78BB2433" = "XNA (Zune)" + "D59BE175-2ED0-4C54-BE3D-CDAA9F3214C8" = "Workflow (VB.NET)" + "DB03555F-0C8B-43BE-9FF9-57896B3C5E56" = "Windows Phone 8/8.1 App (VB.NET)" + "E24C65DC-7377-472B-9ABA-BC803B73C61A" = "Web Site" + "E3E379DF-F4C6-4180-9B81-6769533ABE47" = "ASP.NET MVC 4.0" + "E53F8FEA-EAE0-44A6-8774-FFD645390401" = "ASP.NET MVC 3.0" + "E6FDF86B-F3D1-11D4-8576-0002A516ECE8" = "J#" + "EC05E597-79D4-47f3-ADA0-324C4F7C7484" = "SharePoint (VB.NET)" + "EFBA0AD7-5A72-4C68-AF49-83D382785DCF" = "Xamarin.Android / Mono for Android" + "F135691A-BF7E-435D-8960-F99683D2D49C" = "Distributed System" + "F184B08F-C81C-45F6-A57F-5ABD9991F28F" = "VB.NET" + "F2A71F9B-5D33-465A-A702-920D77279786" = "F#" + "F5B4F3BC-B597-4E2B-B552-EF5D8A32436F" = "MonoTouch Binding" + "F85E285D-A4E0-4152-9332-AB1D724D3325" = "ASP.NET MVC 2.0" + "F8810EC1-6754-47FC-A15F-DFABD2E3FA90" = "SharePoint Workflow" + "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC" = "C#" + }.$ProjectTypeGuid +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsProfilesParameter.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsProfilesParameter.ps1 new file mode 100644 index 0000000..4a8334e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsProfilesParameter.ps1 @@ -0,0 +1,56 @@ +function Get-DynamicAwsProfilesParameter { + + [CmdletBinding()] + param( + + [Parameter(Mandatory = $false)] + [string]$GeneratedParameterName = "ProfileName", + + [Parameter(Mandatory = $false)] + [string]$ParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory = $false)] + [switch]$IsMandatoryParameter + ) + + # Define the Paramater Attributes + $ParameterName = $GeneratedParameterName + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $IsMandatoryParameter + $ParameterAttribute.ParameterSetName = $ParameterSetName + $ParameterAttribute.HelpMessage = "The Local AWS Credential Profile Name to Use for Requests" + $AttributeCollection.Add($ParameterAttribute) + + $argumentCompleterScriptBlock = { + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $awsEntries = Get-AWSConfigEntries + return $awsEntries.Name | Sort-Object | Get-Unique + } + $argumentCompleterAttribute = [System.Management.Automation.ArgumentCompleterAttribute]::new($argumentCompleterScriptBlock) # constructor requires a ScriptBlock or Type parameter + $AttributeCollection.Add($argumentCompleterAttribute) + + # Generate and add the ValidateSet + # Do not print warnings because this may be loaded/evaluated on servers without any profiles + $arrSet = (Get-AWSConfigEntries -WarningAction SilentlyContinue).Name + + # If no profiles are found, set the only available value to NoLocalProfilesFound + if (Test-IsCollectionNullOrEmpty $arrSet) { + + $arrSet = @( "NoLocalProfilesFound" ) + } + + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + + # Create the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string[]], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsRegionParameter.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsRegionParameter.ps1 new file mode 100644 index 0000000..f189374 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-DynamicAwsRegionParameter.ps1 @@ -0,0 +1,36 @@ +function Get-DynamicAwsRegionParameter { + + [CmdletBinding()] + param( + + [Parameter(Mandatory = $false)] + [string]$GeneratedParameterName = "Region", + + [Parameter(Mandatory = $false)] + [string]$ParameterSetName = "__AllParameterSets", + + [Parameter(Mandatory = $false)] + [switch]$IsMandatoryParameter + ) + + # Define the Paramater Attributes + $ParameterName = $GeneratedParameterName + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $IsMandatoryParameter + $ParameterAttribute.ParameterSetName = $ParameterSetName + $ParameterAttribute.HelpMessage = "The AWS Region to Use for Requests" + $AttributeCollection.Add($ParameterAttribute) + + # Generate and add the ValidateSet + $arrSet = Get-SupportedAwsRegions + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + + # Create the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string[]], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-FunctionWriteProgressHelperCalls.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-FunctionWriteProgressHelperCalls.ps1 new file mode 100644 index 0000000..3b19687 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-FunctionWriteProgressHelperCalls.ps1 @@ -0,0 +1,10 @@ +function Get-FunctionWriteProgressHelperCalls { + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$FunctionName + ) + + $commandDefinition = (Show-CommandDefinition $FunctionName) + return ([System.Management.Automation.PsParser]::Tokenize($commandDefinition, [ref]$null) | where { $_.Type -eq 'Command' -and $_.Content -eq 'Write-ProgressHelper' }).Count +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-GitBranchNames.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-GitBranchNames.ps1 new file mode 100644 index 0000000..0db0d00 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-GitBranchNames.ps1 @@ -0,0 +1,26 @@ +function Get-GitBranchNames { +<# +.SYNOPSIS + Get the branch names for a given repository path, if it is a repository + +.PARAMETER Path + The file path to check. Can be presumed as the current folder. +#> + param( + $Path = ((Get-Location).Path) + ) + + $return = @() + + $Path = (Find-GitRepoRootFromPath -Path $Path) + + $branches = (Get-ChildItem -Path (Join-Path -Path $Path -ChildPath ".git\refs\heads" -Resolve) -Recurse -File).FullName + + foreach($branch in $branches) { + # This could be rooted _under_ heads by several folders + # It might be faster to Read-GitConfig and look at branch.name but filesystems are usually pretty fast + $return += ($branch -split 'heads')[1].Substring(1).Replace('\','/') + } + + return ($return | Sort-Object) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandAddDefinition.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandAddDefinition.ps1 new file mode 100644 index 0000000..5c614ce --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandAddDefinition.ps1 @@ -0,0 +1,190 @@ +function Get-GitCommandAddDefinition { +<# +.SYNOPSIS + Get the command definition for the git add verb command + This is useful for validating a git input is reasonably correct +#> + [CmdletBinding()] + [OutputType([object])] + param() + + return @{ + CommandName = 'Add' + Alias = @( + 'add' + ) + Arguments = @( + @{ + ArgumentName = 'Verbose' + Variants = @( + '--verbose' + '-vv' + '-v' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Dry Run' + Variants = @( + '--dry-run' + '-n' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Force' + Variants = @( + '--force' + '-f' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Interactive' + Variants = @( + '--interactive' + '-i' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Patch' + Variants = @( + '--patch' + '-p' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Edit' + Variants = @( + '--edit' + '-e' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'MaybeUpdate' + Variants = @( + '--no-all' + '--all' + '--no-ignore-removal' + '--ignore-removal' + '-A' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @( + @{ + ArgumentName = 'Update' + Variants = @( + '-update' + '-u' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + + ) + } + @{ + ArgumentName = 'IntentToAdd' + Variants = @( + '--intent-to-add' + '-N' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Refresh' + Variants = @( + '--refresh' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'IgnoreErrors' + Variants = @( + '--ignore-errors' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'IgnoreMissing' + Variants = @( + '--ignore-missing' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Renormalize' + Variants = @( + '--renormalize' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'ChangeModificationStrategy' + Variants = @( + '--chmod' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '(+|-){1}.+' + TrailingArguments = @() + } + @{ + ArgumentName = 'PathspecFromFile' + Variants = @( + '--pathspec-from-file' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '.+' + TrailingArguments = @( + @{ + ArgumentName = 'PathspecFileNull' + Variants = @( + '--pathspec-file-nul' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + + ) + } + @{ + ArgumentName = 'RestOfLine' + Variants = @( + '--' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + ) + } + +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandApplyDefinition.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandApplyDefinition.ps1 new file mode 100644 index 0000000..848bca8 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-GitCommandApplyDefinition.ps1 @@ -0,0 +1,243 @@ +function Get-GitCommandApplyDefinition { +<# +.SYNOPSIS + Get the command definition for the git apply verb command + This is useful for validating a git input is reasonably correct + +.NOTES + Not documented here: + [-p] + [-C] + + Not handled because this is a weird scenario to match on given the other constraints + # TODO ~ cbrand - Handle this case +#> + [CmdletBinding()] + [OutputType([object])] + param() + + return @{ + CommandName = 'Apply' + Alias = @( + 'apply' + ) + Arguments = @( + @{ + ArgumentName = 'Verbose' + Variants = @( + '--verbose' + '-v' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Statistics' + Variants = @( + '--stat' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'NumericalStatistics' + Variants = @( + '--numstat' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @( + @{ + ArgumentName = 'NullTerminatedMachineOutput' + Variants = @( + '-z' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + ) + } + @{ + ArgumentName = 'Summary' + Variants = @( + '--summary' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Check' + Variants = @( + '--check' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Index' + Variants = @( + '--index' + '--intent-to-add' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'ThreeWay' + Variants = @( + '--3way' + '-3' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @( + ) + } + @{ + ArgumentName = 'Apply' + Variants = @( + '--apply' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'NoAdd' + Variants = @( + '--no-add' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'BuildFakeAncestor' + Variants = @( + '--build-fake-ancestor' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '.+' + TrailingArguments = @() + } + @{ + ArgumentName = 'Reverse' + Variants = @( + '--reverse' + '-R' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Binary' + Variants = @( + '--binary' + '--allow-binary-replacement' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Reject' + Variants = @( + '--reject' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'InaccurateEOF' + Variants = @( + '--inaccurate-eof' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Recount' + Variants = @( + '--recount' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Cached' + Variants = @( + '--cached' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'IgnoreWhitespace' + Variants = @( + '--ignore-whitespace' + '--ignore-space-change' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + @{ + ArgumentName = 'Whitespace' + Variants = @( + '--whitespace' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = 'nowarn|warn|fix|error|error-all' + TrailingArguments = @() + } + @{ + ArgumentName = 'Exclude' + Variants = @( + '--exclude' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '.+' + TrailingArguments = @() + } + @{ + ArgumentName = 'Include' + Variants = @( + '--include' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '.+' + TrailingArguments = @() + } + @{ + ArgumentName = 'Directory' + Variants = @( + '--directory' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '.+' + TrailingArguments = @() + } + @{ + ArgumentName = 'unsafe-paths' + Variants = @( + '--unsafe-paths' + ) + RequiredAdditionalArguments = 0 + EqualsSplitAcceptRegex = '' + TrailingArguments = @() + } + ) + } + +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-GrandParentFunctionName.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-GrandParentFunctionName.ps1 new file mode 100644 index 0000000..67b0e56 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-GrandParentFunctionName.ps1 @@ -0,0 +1,12 @@ +function Get-GrandParentFunctionName { +<# +.SYNOPSIS + Get the name of the function that called the one that called us +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + return (Get-PSCallStack)[2].Command +} + diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-HackerText.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-HackerText.ps1 new file mode 100644 index 0000000..99c4f9a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-HackerText.ps1 @@ -0,0 +1,150 @@ +function Get-HackerText { + param ( + [string]$Textline, + [switch]$Print, + [switch]$Clipboard + ) + + $words = @() + $currentQuoteDelimiter = '' + $currentWord = @() + $inQuotedString = $false + $lastCharWildcard = $false + + foreach ($char in $Textline -split '') { + $quoteDelimiter = ($char -eq '"' -or $char -eq "'") + + if ($lastCharWildcard) { + $currentWord += $char + $lastCharWildcard = $false + } elseif ($char -eq '`') { + $lastCharWildcard = $true + $currentWord += $char + } elseif ($quoteDelimiter -and -not $inQuotedString) { + # begin quoted string + $inQuotedString = $true + $currentQuoteDelimiter = $char + $currentWord += $char + } elseif ($quoteDelimiter -and $inQuotedString -and $quoteDelimiter -eq $currentQuoteDelimiter) { + # end quoted string + $inQuotedString = $false + $currentQuoteDelimiter = '' + $currentWord += $char + $words += ($currentWord -join '') + $currentWord = @() + } elseif ($char -eq ' ') { + if ($currentWord.Count -gt 0) { + $words += ($currentWord -join '') + } + $currentWord = @() + } else { + $currentWord += $char + } + } + if ($currentWord.Count -gt 0) { + $words += ($currentWord -join '') + } + + $actions = @() + foreach ($word in $words) { + if ($word[0] -in ('"',"'")) { + # a human would just type 6 letters, not paste it + if ($word.Trim().Length -lt 8) { + $actions += @{ Type = " $word" } + } else { + # word is a quoted string, use Paste action + $actions += @{ Type = " $($word[0])" } + $actions += @{ Paste = ($word[1..($word.Length - 2)] -join '') } + $actions += @{ Type = "$($word[-1]) " } + } + } else { + $hyphenSplit = $word -split '-' + if ($hyphenSplit.Count -eq 2) { + if ($hyphenSplit[1].Length -gt 3) { + $type = $hyphenSplit[0] + if ([string]::IsNullOrWhiteSpace($type)) { + $type = ' ' + } + + # special case for special children + if ($word -eq '-ProfileName') { + $actions += @{ Type = " -Pro" } + $actions += @{ Tab = 'file' } + $actions += @{ TabChange = 'Location' } + $actions += @{ Tab = 'Name' } + } else { + $tabSplit = ($hyphenSplit[1][0..2] -join '') + $actions += @{ Type = "$type-$tabSplit" } + $actions += @{ Tab = $hyphenSplit[1].Substring(3) } + } + } else { + $actions += @{ Type = " $word" } + } + } else { + if ($word[0] -eq '$' -and $word.Length -gt 6) { + $actions += @{ Type = " $(($word[0..3] -join ''))" } + $actions += @{ Tab = $word.Substring(4) } + } else { + $actions += @{ Type = " $word" } + } + } + } + } + + $reparsedActions = @() + $reparsingCounter = 0 + while ($true) { + $actionsLength = $actions.Count + for ($i = 0; $i -lt $actionsLength; $i++) { + $action = $actions[$i] + if ($i -lt ($actionsLength - 1)) { + if ($action.Keys[0] -eq $actions[$i + 1].Keys[0]) { + # same key, combine the values + $key = $action.Keys[0] + $value1 = $action.Values[0] + $value2 = $actions[$i + 1].Values[0] + $value = "$value1$value2" -replace ' ',' ' + $reparsedActions += @{ "$key" = "$value" } + $i += 1 + } else { + $reparsedActions += $action + } + } else { + $reparsedActions += $action + } + } + if ($reparsedActions.Count -eq $actions.Count -or $reparsingCounter -gt 5) { + break + } else { + $actions = $reparsedActions.Clone() + $reparsedActions = @() + $reparsingCounter += 1 + } + } + + if ($Print -or $Clipboard) { + $output = @() + $output += "Invoke-ScriptedActions -Actions @(" + foreach ($action in $actions) { + $key = $action.Keys[0] + $value = $action.Values[0] + $stringDelimiter = "'" + if ($value.IndexOf("'") -gt -1) { + $stringDelimiter = '"' + } else { + $value = $value -replace '`','' + } + $output += " @{ $key = $stringDelimiter$value$stringDelimiter }" + } + $output += ")" + + if ($Clipboard) { + $output | Set-Clipboard + } else { + return $output + } + } else { + return $actions + } + +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-HistoryEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-HistoryEntries.ps1 new file mode 100644 index 0000000..7283f4a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-HistoryEntries.ps1 @@ -0,0 +1,62 @@ +function Get-HistoryEntries { +<# +.SYNOPSIS + Get the history according to what was saved by PSReadLine +#> + [CmdletBinding()] + [OutputType([System.Collections.ArrayList])] + param ( + [switch]$SkipMultilines, + [switch]$OnlyMultilines, + [switch]$SkipLongLines, + [Parameter()] + [string[]]$AdditionalPath + ) + + $logLead = Get-LogLeadName + $longLineLength = 120 # 120 characters is a pretty long line + + if ($SkipMultilines -and $OnlyMultilines) { + throw "$logLead : You can't specify both skip and only multilines" + } + + try { + $path = (Get-PSReadlineOption).HistorySavePath + } catch { + Write-Warning "$logLead : Is PSReadLine installed? Can't find the history save path" + return + } + + $lines = @() + foreach ($filePath in $AdditionalPath) { + $lines += Get-Content -Path $filePath + } + + $lines += Get-Content -Path $path + + $records = New-Object -TypeName "System.Collections.ArrayList" + + $isMultiline = $false + $groupedLines = @() + foreach ($line in $lines) { + if ($line.EndsWith('`')) { + $line = $line.TrimEnd('`') + $groupedLines += $line + $isMultiline = $true + continue + } + + $groupedLines += $line + $lineIsLong = ($groupedLines -join '').Length -gt $longLineLength + $skipAdd = ($SkipLongLines -and $lineIsLong) -or ($OnlyMultilines -and !$isMultiline) -or ($SkipMultilines -and $isMultiline) + + if (!$skipAdd) { + $records.Add($groupedLines) | Out-Null + } + + $groupedLines = @() + $isMultiline = $false + } + + return $records +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-HostsFileAllRecords.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-HostsFileAllRecords.ps1 new file mode 100644 index 0000000..b89e06c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-HostsFileAllRecords.ps1 @@ -0,0 +1,18 @@ +function Get-HostsFileAllRecords { +<# +.SYNOPSIS + Returns all hosts file entries as a list of objects of the format: + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.OUTPUTS + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.LINK + ConvertTo-HostsFileEntry +#> + [CmdletBinding()] + [OutputType([object[]])] + param() + + return (Get-Content -Path (Get-HostsFilePath)) | ConvertTo-HostsFileEntry +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-HostsFilePath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-HostsFilePath.ps1 new file mode 100644 index 0000000..53e63c0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-HostsFilePath.ps1 @@ -0,0 +1,15 @@ +function Get-HostsFilePath { +<# +.SYNOPSIS + Return the known location of the hosts file. This function is platform aware. +#> + [CmdletBinding()] + [OutputType([string])] + param() + + if (Test-IsWindowsPlatform) { + return "$env:windir\System32\drivers\etc\hosts" + } else { + return "/etc/hosts" + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-IFConfig.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-IFConfig.ps1 new file mode 100644 index 0000000..9b9a534 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-IFConfig.ps1 @@ -0,0 +1,143 @@ +function Get-IFConfig { +<# +.SYNOPSIS + Because sometimes I forget what OS I'm on, so I type ifconfig when I meant ipconfig + So this is just a passthrough in PowerShell style to ipconfig instead + TODO: Make it actually do an ifconfig style input/output instead of ipconfig + +.NOTES + Adapter name doesn't allow wildcards. Sorry for the inconvenience. + +.PARAMETER All + Display full configuration information. + +.PARAMETER Release + Release the IPv4 address for the specified adapter. + +.PARAMETER Release6 + Release the IPv6 address for the specified adapter. + +.PARAMETER Renew + Renew the IPv4 address for the specified adapter. + +.PARAMETER Renew6 + Renew the IPv6 address for the specified adapter. + +.PARAMETER FlushDNS + Purges the DNS Resolver cache. + +.PARAMETER RegisterDNS + Refreshes all DHCP leases and re-registers DNS names + +.PARAMETER DisplayDNS + Display the contents of the DNS Resolver Cache. + +.PARAMETER ShowClassID + Displays all the dhcp class IDs allowed for adapter. + +.PARAMETER SetClassID + Modifies the dhcp class id. + +.PARAMETER ShowClassID6 + Displays all the IPv6 DHCP class IDs allowed for adapter. + +.PARAMETER SetClassID6 + Modifies the IPv6 DHCP class id. +#> + [CmdletBinding(DefaultParameterSetName = 'All')] + param ( + # This can be used by all commands, the rest can't + [switch]$AllCompartments, + [Parameter(ParameterSetName = 'All')] + [switch]$All, + [Parameter(ParameterSetName = 'FlushDNS')] + [switch]$FlushDNS, + [Parameter(ParameterSetName = 'RegisterDNS')] + [switch]$RegisterDNS, + [Parameter(ParameterSetName = 'DisplayDNS')] + [switch]$DisplayDNS, + + [Parameter(ParameterSetName = 'Release')] + [string]$Release, + [Parameter(ParameterSetName = 'Release6')] + [string]$Release6, + [Parameter(ParameterSetName = 'Renew')] + [string]$Renew, + [Parameter(ParameterSetName = 'Renew6')] + [string]$Renew6, + [Parameter(ParameterSetName = 'ShowClassID')] + [string]$ShowClassID, + [Parameter(ParameterSetName = 'ShowClassID6')] + [string]$ShowClassID6, + + [Parameter(ParameterSetName = 'SetClassID')] + [string]$SetClassID, + [Parameter(ParameterSetName = 'SetClassID6')] + [string]$SetClassID6, + + [Parameter(ParameterSetName = 'SetClassID', ValueFromRemainingArguments = $true)] + [Parameter(ParameterSetName = 'SetClassID6', ValueFromRemainingArguments = $true)] + [string[]]$ClassID + ) + + $logLead = Get-LogLeadName + + $splat = @{ + Path = "C:\WINDOWS\system32\ipconfig.exe" + Arguments = @() + } + + if ($null -ne $ClassID) { + $ClassID = @($ClassID)[0] + } + + $lines = ipconfig + + $adapters = @() + foreach ($line in $lines) { + if ($line.StartsWith("Ethernet adapter")) { + $adapters += $line.Substring(16).Trim().TrimEnd(':') + } + } + + # Only one of these values can be supplied in the first place, so it is a lot of empty strings concat together + $adapterString = "$Release$Release6$Renew$Renew6$ShowClassID$ShowClassID6$SetClassID$SetClassID6" + if (![string]::IsNullOrWhiteSpace($adapterString)) { if ($adapters -notcontains $adapterString) { + Write-Warning "$logLead : Adapter string given was not a present adapter. Choose from: `n`t$($adapters -join "`n`t")" + throw "$logLead : Adapter string given was not a present adapter." + }} + + switch ($PSCmdlet.ParameterSetName) { + # Switches + 'All' { $splat.Arguments += "/all"} + 'FlushDNS' { $splat.Arguments += "/flushdns"} + 'RegisterDNS' { $splat.Arguments += "/registerdns"} + 'DisplayDNS' { $splat.Arguments += "/displaydns"} + + # Valid variables required + 'Release' { $splat.Arguments += "/release $Release"} + 'Release6' { $splat.Arguments += "/release6 $Release6"} + 'Renew' { $splat.Arguments += "/renew $Renew"} + 'Renew6' { $splat.Arguments += "/renew6 $Renew6"} + 'ShowClassID' { + if ([string]::IsNullOrWhiteSpace($ShowClassID)) { throw "$logLead : adapter name must be specified" } + $splat.Arguments += "/showclassid $ShowClassID" + } + 'ShowClassID6' { + if ([string]::IsNullOrWhiteSpace($ShowClassID6)) { throw "$logLead : adapter name must be specified" } + $splat.Arguments += "/showclassid6 $ShowClassID6" + } + + # Multiple variables set + 'SetClassID' { + if ([string]::IsNullOrWhiteSpace($SetClassID)) { throw "$logLead : adapter name must be specified" } + $splat.Arguments += "/setclassid $SetClassID $ClassID" + } + 'SetClassID6' { + if ([string]::IsNullOrWhiteSpace($SetClassID6)) { throw "$logLead : adapter name must be specified" } + $splat.Arguments += "/setclassid6 $SetClassID6 $ClassID" + } + } + + Invoke-CallOperatorWithPathAndParameters @splat +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-JiraBaseUrl.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-JiraBaseUrl.ps1 new file mode 100644 index 0000000..c8ba7d4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-JiraBaseUrl.ps1 @@ -0,0 +1,21 @@ +function Get-JiraBaseUrl { +<# +.SYNOPSIS + Return the base Jira url. + May read the value from an environment variable if present +#> + [CmdletBinding()] + [OutputType([string])] + param ( + ) + $logLead = (Get-LogLeadName) + + $environmentVariable = (Get-EnvironmentVariable "JIRA_URL" 6>$null 5>$null 4>$null 3>$null) + if (![string]::IsNullOrWhiteSpace($environmentVariable)) { + Write-Information "$logLead : Returning environment variable" + return $environmentVariable + } + + Write-Information "$logLead : Returning default value" + return "https://jira.alkami.com/" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-JiraBearerTokenAuthWebHeader.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-JiraBearerTokenAuthWebHeader.ps1 new file mode 100644 index 0000000..640a3b7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-JiraBearerTokenAuthWebHeader.ps1 @@ -0,0 +1,21 @@ +function Get-JiraBearerTokenAuthWebHeader { + [CmdletBinding()] + [OutputType([object])] + param( + [Parameter(Mandatory = $false)] + [string]$Token + ) + + $logLead = (Get-LogLeadName) + + if ([string]::IsNullOrWhiteSpace($Token)) { + $Token = Get-EnvironmentVariable -Name "JIRA_BEARERTOKEN" + } + + if ([string]::IsNullOrWhiteSpace($Token)) { + Write-Warning "$logLead : No bearer token found. Please visit your profile page in Jira to generate one and then store with `n`n`tSet-JiraBearerToken -Token -StoreName User`n" + throw "$logLead : No bearer token found or provided." + } + + return @{ Authorization = "Bearer $Token" } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-JiraTeams.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-JiraTeams.ps1 new file mode 100644 index 0000000..873e4e7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-JiraTeams.ps1 @@ -0,0 +1,149 @@ +function Get-JiraTeams { +<# +.SYNOPSIS + Get all the team members for each team per what is listed in Jira (tempo) +#> + [CmdletBinding()] + param ( + [switch]$RefreshCache + ) + + $logLead = (Get-LogLeadName) + + $cacheFilePath = (Get-CachePathJiraTeams) + + if ($RefreshCache) { + $cacheFilePath = (Get-CachePathJiraTeams) + if (Test-Path -Path $cacheFilePath) { + Remove-Item -Path $cacheFilePath -Force + } + } + + if (Test-Path -Path $cacheFilePath) { + $cachedData = ConvertFrom-Json (Get-Content -Path $cacheFilePath -Raw) + return ($cachedData | ConvertTo-JiraTeam) + } + + $logLead = (Get-LogLeadName) + + $headers = (Get-JiraBearerTokenAuthWebHeader) + $headers["Content-Type"] = "application/json" + $headers["Accept"] = "application/json" + + # This gives all the teams available to be picked from + # The / 2 / is "I can browse teams to select them" + # If it were / 1 / it would be "I am a tempo-teams administrator" + $teamsUrl = (Join-UrlComponents -BaseUrl $url -Path "/rest/tempo-teams/2/team" -Query @{ expand = "projects" }) + # $t.Where({!$_.name.StartsWith('zz')-and $_.program-eq 'Development'}) | % { "'$($_.Name)' { $($_.id) }" } | Set-Clipboard + + $arguments = @{ + Headers = $headers + Uri = $teamsUrl + Method = 'GET' + } + + try { + $allTeams = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + + $teams = @() + + foreach ($team in $allTeams) { + $teamName = $team.name + if ($teamName.StartsWith("zz")) { + Write-Host "$logLead : Skipping team [$teamName] as it is a 'zz' team" + continue + } + $department = $team.program + if ($department -eq "Archived") { + Write-Host "$logLead : Skipping team [$teamName] as it is archived" + continue + } + $teamMission = $team.mission + $lead = $team.lead + $summary = $team.summary + $links = @() + + $teamlinksUrl = (Join-UrlComponents -BaseUrl $url -Path "/rest/tempo-teams/2/team/$($team.id)/link" -Query @{ expand = "projects" }) + $arguments = @{ + Headers = $headers + Uri = $teamlinksUrl + Method = 'GET' + } + try { + $teamLinks = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + foreach ($link in $teamLinks.Where({$_.scopeType -eq 'board'})) { + $links += @{ + TeamName = $link.teamName + Board = $link.scope + } + } + + $teamMembersUrl = (Join-UrlComponents -BaseUrl $url -Path "/rest/tempo-teams/2/team/$($team.id)/member" -Query @{ expand = "projects" }) + $arguments = @{ + Headers = $headers + Uri = $teamMembersUrl + Method = 'GET' + } + try { + $teamMembers = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + + $members = @() + + foreach($member in $teamMembers) { + if ($member.member.key -eq $lead) { + $lead = $member.member.name + } + $members += @{ + Name = $member.member.name + Role = $member.membership.role.name + DisplayName = $member.member.displayname + Inactive = !$member.member.activeInJira + } + } + + $teams += @{ + Name = $teamName + Summary = $summary + Department = $department + Mission = $teamMission + Lead = $lead + AllMembers = $members + Members = $members + Manager = @{} + ScrumMaster = @{} + ProductOwner = @{} + ProjectManager = @{} + Links = $links + # RawMembers = $teamMembers + } + } + + $allScrumMasters = $teams.AllMembers.Where({$_.Role -eq "Scrum Masters"}) + + foreach ($team in $teams) { + $team.Manager = $team.AllMembers.Where({$_.Role -eq "Manager" -and $_.Name -notin $allScrumMasters.Name}) + $team.ProjectManager = $team.AllMembers.Where({$_.Role -eq "Project Manager" -and $_.Name -notin $allScrumMasters.Name}) + $team.ProductOwner = $team.AllMembers.Where({$_.Role -eq "Product Owner" -and $_.Name -notin $allScrumMasters.Name}) + $team.ScrumMaster = $team.AllMembers.Where({($_.Role -eq "Manager" -or $_.Role -eq "Scrum Masters") -and $_.Name -in $allScrumMasters.Name}) + $team.Members = $team.AllMembers.Where({$_.Name -ne $team.Manager.Name -and $_.Name -ne $team.ScrumMaster}) + } + + ConvertTo-Json $teams -Depth 10 | Set-Content -Path $cacheFilePath + + return ($teams | ConvertTo-JiraTeam) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-JiraTicketMetaFields.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-JiraTicketMetaFields.ps1 new file mode 100644 index 0000000..ef384bc --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-JiraTicketMetaFields.ps1 @@ -0,0 +1,88 @@ +function Get-JiraTicketMetaFields { + param ( + $ProjectKey = 'DEV', + $IssueType = 'Story' + ) + + # This API should be anonymously fetchable + # $Credential = (Get-CredentialFromEnvironmentVariables) + # $headers = (Get-BasicAuthWebHeader -Credential $Credential) + + $headers = (Get-JiraBearerTokenAuthWebHeader) + $headers["Content-Type"] = "application/json" + $headers["Accept"] = "application/json" + +<# + $arguments = @{ + Headers = $headers + Uri = "https://jira.alkami.com/rest/api/latest/issue/DEV-120482" + Method = 'GET' + } + + try { + $response = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + + return $response +#> +#<# + $url = (Get-JiraBaseUrl) + + # Get the project issue types for the specified project + $jiraUrlMetaCreate = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue/createmeta" -Query @{ projectKeys = $ProjectKey }) + + Write-Host $jiraUrlMetaCreate + + $arguments = @{ + Headers = $headers + Uri = $jiraUrlMetaCreate + Method = 'GET' + } + + try { + $response = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + + $projectTypeId = $response.Projects.issueTypes.Where({$_.name -eq $IssueType}).id + + if ([string]::IsNullOrWhiteSpace($projectTypeId)) { + Write-Error "$logLead : ProjectTypeId for [$ProjectKey]::[$IssueType] not found" + return $response + } +#> + $url = (Get-JiraBaseUrl) + + $jiraUrlMetaCreateFields = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue/createmeta" -Query @{ projectKeys = $ProjectKey; projectTypeIds = $projectTypeId; expand = "projects.issuetypes.fields"}) + Write-Host $jiraUrlMetaCreateFields + + $arguments = @{ + Headers = $headers + Uri = $jiraUrlMetaCreateFields + Method = 'GET' + } + + try { + $response = Invoke-RestMethod @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } + + return $response.projects.issueTypes.Where({$_.Name -eq 'Story'}) +} + +<# + + +https://jira.alkami.com/rest/default/1.0/values/list.json?project=11000&issuetype=10601&admin=false + +#> \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-Json.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-Json.ps1 new file mode 100644 index 0000000..4bddb2f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-Json.ps1 @@ -0,0 +1,44 @@ +function Get-Json { +<# +.SYNOPSIS + I get tired of having to type the right syntax to get the json object from a file + +.PARAMETER Path + The file path + +.PARAMETER AsHash + Convert the thing to an iterable Hash +#> + [CmdletBinding(DefaultParameterSetName = "FilePath")] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = "FilePath", Position = 0)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory = $false, ParameterSetName = "FilePath")] + [switch]$AsHash, + [Parameter(Mandatory = $true, ParameterSetName = "JsonBlob")] + [ValidateNotNullorEmpty()] + [object]$JsonBlob + ) + + $logLead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq "FilePath") { + if (!(Test-Path $Path)) { + throw "$logLead : [$Path] does not exist" + } + + $JsonBlob = (ConvertFrom-Json (Get-Content $Path -Raw)) + + if (!$AsHash) { + return $JsonBlob + } + } + + if ($JsonBlob -is [string]) { + $JsonBlob = (ConvertFrom-Json $JsonBlob) + } + + return (ConvertTo-Hashtable $JsonBlob) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-JsonStringLeadsByDepth.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-JsonStringLeadsByDepth.ps1 new file mode 100644 index 0000000..7caa779 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-JsonStringLeadsByDepth.ps1 @@ -0,0 +1,21 @@ +function Get-JsonStringLeadsByDepth { +<# +.SYNOPSIS + Returns the indented or leftdented spaces for the level, in that order + +.OUTPUTS + [$spacesString, $shortSpacesString] + [Indented string, Leftdented string] +#> + param ( + $Depth = 0 + ) + + $spacesString = [string]::new(" ", ($Depth + 1) * 2) + $shortSpacesString = "" + if ($Depth -gt 0) { + $shortSpacesString = [string]::new(" ", $Depth * 2) + } + + return ($spacesString, $shortSpacesString) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-KnownDeveloperHostsEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-KnownDeveloperHostsEntries.ps1 new file mode 100644 index 0000000..b23963b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-KnownDeveloperHostsEntries.ps1 @@ -0,0 +1,29 @@ +function Get-KnownDeveloperHostsEntries { +<# +.SYNOPSIS + Returns a list of known developer host entries for ORB installation default webapps +#> + [CmdletBinding()] + [OutputType([string])] + param() + + return @( + @{ IpAddress = "127.0.0.1"; Hostname = "AuditService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "BankService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ContentService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "CoreService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ExceptionService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "IP-STS"; } + @{ IpAddress = "127.0.0.1"; Hostname = "MessageCenterService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "NagConfigurationService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "NotificationService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "RP-STS"; } + @{ IpAddress = "127.0.0.1"; Hostname = "Scheduler"; } + @{ IpAddress = "127.0.0.1"; Hostname = "SecurityManagementService"; } + @{ IpAddress = "127.0.0.1"; Hostname = "STSConfiguration"; } + @{ IpAddress = "127.0.0.1"; Hostname = "redis-18620.redis.corp.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "ip.dev.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "developer.dev.alkamitech.com"; } + @{ IpAddress = "127.0.0.1"; Hostname = "admin-developer.dev.alkamitech.com"; } + ) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-LastWebRequestErrorText.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-LastWebRequestErrorText.ps1 new file mode 100644 index 0000000..c8e92ff --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-LastWebRequestErrorText.ps1 @@ -0,0 +1,59 @@ +function Get-LastWebRequestErrorText { +<# +.SYNOPSIS + Finds the last error in the errors stack that has a response stream and reads the value. + Obviously this only works for open/current sessions, as this is an in memory variable that can be wiped at any time. + +.PARAMETER All + [switch] Finds all errors and writes them out serially from most recent to last +#> + [CmdletBinding()] + [OutputType([string])] + param ( + [switch]$All + ) + + $logLead = (Get-LogLeadName) + $allErrors = @($error) + + if (($allErrors.Count -eq 0) -or ($allErrors[0] -eq $null)) { + Write-Host "$logLead : No errors found or first entry is null. This is unexpected. Can not continue." + return $null + } + + # Setup some place to track the values we found + $foundStreams = @() + + # Ensure each error has been processed to see if it has a value that can be used + foreach ($anyError in $allErrors) { + if ($anyError.ResponseRecorded) { + $foundStreams += $anyError.ResponseFoundStream + continue + } + + $response = $anyError.Exception.Response + if ($null -ne $response) { + # This only happens for things that have a response and that haven't been processed yet + # That is to say, the cycle spin time here should be short + $responseStream = $response.GetResponseStream() + $foundStream = $null + if ($null -ne $responseStream) { + $foundStream = (Read-StreamAsString $responseStream) + $foundStreams += $foundStream + } + $anyError | Add-Member -NotePropertyName "ResponseFoundStream" -NotePropertyValue $foundStream + $anyError | Add-Member -NotePropertyName "ResponseRecorded" -NotePropertyValue $true + } + } + + if ($foundStreams.Count -eq 0) { + Write-Warning "$logLead : Could not find any errors with response streams to parse." + return $null + } + + if (!$All) { + return $foundStreams[0] + } else { + return $foundStreams + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-LocalCachedAWSProfile.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-LocalCachedAWSProfile.ps1 new file mode 100644 index 0000000..9b8e927 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-LocalCachedAWSProfile.ps1 @@ -0,0 +1,12 @@ +function Get-LocalCachedAWSProfile { +<# +.SYNOPSIS + Used to retrieve the locally cached AWS profile from the environment store +#> + [CmdletBinding()] + [OutputType([string])] + param ( + ) + + return ((Get-EnvironmentVariable -Name "USERCACHE_AWSProfileKey" -StoreName User) 6>$null 5>$null 4>$null 3>$null) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-LocalConfiguredAWSProfileNames.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-LocalConfiguredAWSProfileNames.ps1 new file mode 100644 index 0000000..d13b12f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-LocalConfiguredAWSProfileNames.ps1 @@ -0,0 +1,29 @@ +function Get-LocalConfiguredAWSProfileNames { +<# +.SYNOPSIS + Get the locally configured AWS Profile Names for the current user +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + $CredentialsFilePath = '~/.aws/credentials' + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-Path $CredentialsFilePath)) { + throw "$logLead : Can't find your credentials file at [$CredentialsFilePath]. Do you need to configure one? You can run [Register-AWSCredentials]." + } + + $lines = (Get-Content -Path $CredentialsFilePath) + + $profileNames = @() + + foreach($line in $lines) { + if ($line -match "\[[\w-]+\]") { + $profileNames += $line.Substring(1,$line.Length - 2) + } + } + + return $profileNames +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDriveRoots.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDriveRoots.ps1 new file mode 100644 index 0000000..a0c2bd0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDriveRoots.ps1 @@ -0,0 +1,23 @@ +function Get-LocalHardDriveRoots { +<# +.SYNOPSIS + Get the local hard drive filesystem roots for this computer + +.PARAMETER IncludeRemovableDisks + Get the IDs of removable disks as well + +.OUTPUTS + Returns a [string[]] of drive letters. + +.EXAMPLE +> Get-LocalHardDriveRoots +C:,D:,E: +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + $IncludeRemovableDisks + ) + + return @((Get-LocalHardDrives -IncludeRemovableDisks:$IncludeRemovableDisks).DeviceID) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDrives.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDrives.ps1 new file mode 100644 index 0000000..04918b0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-LocalHardDrives.ps1 @@ -0,0 +1,44 @@ +function Get-LocalHardDrives { +<# +.SYNOPSIS + Get the local hard drive information for this computer + +.PARAMETER IncludeRemovableDisks + Get the information of removable disks as well + +.OUTPUTS + Returns a [object[]] of drive info. See also ConvertTo-DriveInfo +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + $IncludeRemovableDisks + ) + + <# + Drive types for the CIM Instance response are: + 0 = Unknown + 1 = No Root directory + 2 = Removable Disk + 3 = Local Disk + 4 = Network Drive + 5 = Compact Disc + 6 = Ram Disk + #> + $driveTypeFilter = @(3) + $return = @() + + if ($IncludeRemovableDisks) { + $driveTypeFilter += 2 + } + + $drives = (Get-CIMInstance -Class Win32_LogicalDisk) + + foreach ($drive in $drives) { + if ($driveTypeFilter -contains $drive.DriveType) { + $return += (ConvertTo-DriveInfo -CimInstance $drive) + } + } + + return $return +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-MostUsedCommand.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-MostUsedCommand.ps1 new file mode 100644 index 0000000..df27d5f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-MostUsedCommand.ps1 @@ -0,0 +1,43 @@ +function Get-MostUsedCommand { +<# +.SYNOPSIS + See your most used single-line commands. + +.DESCRIPTION + Hobby projects are fun, yeah? + +.PARAMETER Top + How many events do you want to see? +#> + [CmdletBinding(DefaultParameterSetName = 'Top')] + [OutputType([string[]])] + param( + [Parameter(ParameterSetName = 'Top')] + [int]$Top = 10, + [Parameter(ParameterSetName = 'MoreThan')] + [int]$MoreThan = 20, + [Parameter()] + [string[]]$AdditionalPath + ) + + $historyEntries = Get-HistoryEntries -SkipMultilines -AdditionalPath $AdditionalPath + + $map = @{} + foreach ($entry in $historyEntries) { + $key = $entry.Trim() + if ($null -eq $map["$key"]) { + $map["$key"] = 0 + } + $map["$key"] = $map["$key"] + 1 + } + + if ($PSCmdlet.ParameterSetName -eq 'Top') { + $show = $map.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First $Top + } + + if ($PSCmdlet.ParameterSetName -eq 'MoreThan') { + $show = ($map.GetEnumerator() | Where-Object { $_.Value -gt $MoreThan }).GetEnumerator() | Sort-Object -Property Value -Descending + } + + $show | Format-Table -Property Value, Name -Autosize +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath.ps1 new file mode 100644 index 0000000..9a3d50f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath.ps1 @@ -0,0 +1,83 @@ +function Get-NormalizedPath { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + [Parameter(Mandatory = $false)] + [string]$ComputerName = (hostname) + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-Path -IsValid -Path $FilePath)) { + Write-Warning "$logLead : Presented file path [$FilePath] is not valid, and can not be parsed. Returning input string." + return $FilePath + } + + $computerNameIsLocalComputer = (Compare-StringToLocalMachineIdentifiers -StringToCheck $ComputerName) + $networkComputerNameIsLocalComputer = $false + + $remoteDriveName = $null + $networkComputerName = $null + + $executingLocation = (Get-Location) + $executingQualifier = (Split-Path ($executingLocation.Drive.Root) -Qualifier) + + $pathQualifier = (Split-Path -Path $FilePath -Qualifier -ErrorAction SilentlyContinue) + $pathQualifierPresent = ([string]::IsNullOrWhiteSpace($pathQualifier)) + + $isNetworkPath = $FilePath.StartsWith("\\") + + $pathRemainder = "" + + if ($isNetworkPath) { + $filePathSplits = $FilePath.Split("\",[StringSplitOptions]::RemoveEmptyEntries) + $networkComputerName = $filePathSplits[0] + $networkComputerNameIsLocalComputer = (Compare-StringToLocalMachineIdentifiers -StringToCheck $networkComputerName) + if ($networkComputerNameIsLocalComputer -and $computerNameIsLocalComputer) { + Write-Verbose "$logLead : Provided path is the local computer, resolving is just gonna be fast." + return $FilePath + } + + # Let's figure out what the path would've been on that computer, and apply it to this computer as a local path + $potentialDriveName = $filePathSplits[1] + if ([string]::IsNullOrWhiteSpace($potentialDriveName)) { + throw "$logLead : Can't parse the network computer name alone as a path. Please provide a more complete path." + } + $remoteDriveNameIsProbablyDrive = (![string]::IsNullOrWhiteSpace($potentialDriveName) -and $potentialDriveName.Length -eq 2 -and $potentialDriveName.EndsWith('$')) + if ($remoteDriveNameIsProbablyDrive) { + $pathQualifier = $potentialDriveName.Replace('$',':') + } else { + throw "$logLead : Can't parse the second segment of the network path as a drive letter (1 character and a `$). Please provide a path that matches this pattern." + } + if ($filePathSplits.Count -gt 2) { + $pathRemainder = $filePathSplits[2] + for($i = 3; $i -le $filePathSplits.Count; $i++) { + $pathRemainder = (Join-Path $pathRemainder $filePathSplits[$i]) + } + } + } else { + $pathRemainder = (Split-Path -Path $FilePath -NoQualifier) + } + + $pathRemainder = $pathRemainder.TrimStart('\') + + if ([string]::IsNullOrWhiteSpace($pathQualifier)) { + $pathQualifier = $executingQualifier + } + + $finalPath = "" + # Return the local path if UNC pathing isn't required. + if($computerNameIsLocalComputer) { + $finalPath = (Join-Path $pathQualifier $pathRemainder) + } else { + $remoteQualifier = $pathQualifier.Replace(':','$') + $finalPath = "\\$ComputerName\$remoteQualifier\$pathRemainder" + } + + $finalPath = $finalPath.TrimEnd('\') + + Write-Verbose "$logLead : Determined final path to be [$finalPath]" + return $finalPath +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/LocalPath.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/LocalPath.pester.ps1 new file mode 100644 index 0000000..58b0916 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/LocalPath.pester.ps1 @@ -0,0 +1,32 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "LocalPaths" { + $computerName = "localhost" + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ParameterFilter { $StringToCheck -eq $computerName } -MockWith { return $true } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ParameterFilter { $StringToCheck -ne $computerName } -MockWith { return $false } + + Context "Local path and local computer" { + $result = Get-NormalizedPath -FilePath "C:\abc\123" -ComputerName $computerName + + It "matches" { + $result | Should -Be "C:\abc\123" + } + } + + Context "Remote path and local computer" { + $result = Get-NormalizedPath -FilePath "\\otherComputer\D$\abc\123" -ComputerName $computerName + + It "matches" { + $result | Should -Be "D:\abc\123" + } + } + + Context "Remote path and local computer - alternate case accepted" { + $result = Get-NormalizedPath -FilePath "\\otherComputer\D$\abc\123" -ComputerName $computerName + + It "matches" { + $result | Should -Be "d:\abc\123" + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/NetworkPath.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/NetworkPath.pester.ps1 new file mode 100644 index 0000000..d27dcda --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-NormalizedPath/NetworkPath.pester.ps1 @@ -0,0 +1,24 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "NetworkPaths" { + $remoteComputerName = "RemoteComputer" + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ParameterFilter { $StringToCheck -eq $remoteComputerName } -MockWith { return $false } + Mock -CommandName Compare-StringToLocalMachineIdentifiers -ParameterFilter { $StringToCheck -ne $remoteComputerName } -MockWith { return $true } + + Context "Local path and remote computer" { + $result = Get-NormalizedPath -FilePath "C:\abc\123" -ComputerName $remoteComputerName + + It "matches" { + $result | Should -Be "\\$remoteComputerName\C$\abc\123" + } + } + + Context "Remote path and remote computer" { + $result = Get-NormalizedPath -FilePath "\\otherComputer\D$\abc\123" -ComputerName $remoteComputerName + + It "matches" { + $result | Should -Be "\\$remoteComputerName\D$\abc\123" + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ParentFunctionName.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ParentFunctionName.ps1 new file mode 100644 index 0000000..08080ee --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ParentFunctionName.ps1 @@ -0,0 +1,12 @@ +function Get-ParentFunctionName { +<# +.SYNOPSIS + Get the name of the function that called this one +#> + [CmdletBinding()] + [OutputType([System.String])] + Param() + + return (Get-PSCallStack)[1].Command +} + diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ProgramDataPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ProgramDataPath.ps1 new file mode 100644 index 0000000..c750082 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ProgramDataPath.ps1 @@ -0,0 +1,18 @@ +function Get-ProgramDataPath { + [CmdletBinding()] + param () + + $path = [Environment]::GetFolderPath("CommonApplicationData") + + if (!(Test-Path $path)) { + return $path + } + + if (Test-IsWindowsPlatform) { + return $env:ProgramData + } else { + # Must be a macOS or Linux system + # Caution, this folder may not exist if not properly setup on the system in question + return "/var" + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-RepoCheckpointPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-RepoCheckpointPath.ps1 new file mode 100644 index 0000000..a3d2ec0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-RepoCheckpointPath.ps1 @@ -0,0 +1,11 @@ +function Get-RepoCheckpointPath { +<# +.SYNOPSIS + Get the path used by the Checkpoint-AllAvailableRepos and other places +#> + [CmdletBinding()] + [OutputType([string])] + param() + + return Get-EnvironmentVariable "Checkpoint_DefaultPath" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-SSHConfigEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-SSHConfigEntries.ps1 new file mode 100644 index 0000000..0550941 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-SSHConfigEntries.ps1 @@ -0,0 +1,120 @@ +function Get-SSHConfigEntries { +<# +.SYNOPSIS + This function is used to get the specified user's SSH config entries as a hashtable for ease of parsing + +.PARAMETER Username + [string] The user to get the ssh config for + +.PARAMETER Path + [string] The path to the config file to read in +#> + [CmdletBinding(DefaultParameterSetName = 'Username')] + [OutputType([object[]])] + param ( + [Parameter(ParameterSetName = 'Username')] + [string]$Username = (Get-CurrentUsername), + + [Parameter(ParameterSetName = 'Path', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + $logLead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'Username') { + $magicPathRoot = '~\.ssh' + $magicPath = (Join-Path -Path $magicPathRoot -ChildPath 'config') + + if (!(Test-Path -Path $magicPathRoot) -or !(Test-Path -Path $magicPath)) { + Write-Warning "$logLead : No SSH config file found for current user" + return $null + } else { + $Path = $magicPath + } + } + + if (!(Test-Path -Path $Path)) { + Write-Warning "$logLead : No SSH config file found at [$Path]" + return $null + } + + $lines = (Get-Content -Path $Path) + +<# +Format of SSH client config file ssh_config +The ssh_config client configuration file has the following format. Both the global /etc/ssh/ssh_config and per-user ~/ssh/config have the same format. + +Empty lines and lines starting with '#' are comments. + +Each line begins with a keyword, followed by argument(s). + +Configuration options may be separated by whitespace or optional whitespace and exactly one =. + +Arguments may be enclosed in double quotes (") in order to specify arguments that contain spaces. +#> + + $currentObject = @{ Comments = @(); } + $return = @() + foreach ($line in $lines) { + $rawLine = $line + $line = $line.Trim() + if ([string]::IsNullOrWhiteSpace($line)) { + if (($currentObject.Comments.Count -gt 0) -or ($null -ne $currentObject.Host)) { + # We have either an object or a random block of comments. + # Assume it's a "previous record" and put it on the $return, then gen up a new object and keep going + $return += $currentObject + $currentObject = @{ Comments = @(); } + } + continue + } + + $value = '' + + $commentStartingIndex = $line.IndexOf('#') + $comment = '' + if ($commentStartingIndex -gt -1) { + $comment = $line.Substring($commentStartingIndex + 1).Trim() + $line = $line.Substring(0, $commentStartingIndex).Trim() + } + + if ([string]::IsNullOrWhiteSpace($line)) { + $currentObject.Comments += $comment + + continue + } + + $quotedIdentifierStartingIndex = $line.IndexOf('"') + $quotedIdentifierEndingIndex = $line.LastIndexOf('"') + if (($quotedIdentifierStartingIndex -gt -1) -and ($quotedIdentifierEndingIndex -gt $quotedIdentifierStartingIndex)) { + # There is a value on the line that starts and ends with a quote mark, so we should escape that into a variable for now + $value = $line.Substring($quotedIdentifierStartingIndex + 1, $quotedIdentifierEndingIndex - $quotedIdentifierStartingIndex - 1) + $line = $line.Substring(0, $quotedIdentifierStartingIndex).Trim() + } + + if ([string]::IsNullOrWhiteSpace($value)) { + $firstSpaceIndex = $line.Trim().IndexOf(' ') + if ($firstSpaceIndex -gt -1) { + $value = $line.Trim().Substring($firstSpaceIndex + 1) + $line = $line.Trim().Substring(0, $firstSpaceIndex) + } + } + + if ([string]::IsNullOrWhiteSpace($line)) { + Write-Host "$logLead : Somehow we have ended up with an empty key name for [$rawLine] with a value [$value] and potential comment [$comment]" + } else { + Write-Verbose "$logLead : We have ended up with a key name for [$line] with a value [$value] and potential comment [$comment]" + $currentObject.$line = $value + if (![string]::IsNullOrWhiteSpace($comment)) { + $currentObject.$line | Add-Member -NotePropertyName 'Comment' -NotePropertyValue $comment + } + } + } + + if (($currentObject.Comments.Count -gt 0) -or ($null -ne $currentObject.Host)) { + # We ended with something that hasn't been appended to the return value yet. + $return += $currentObject + } + + return $return +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeams.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeams.ps1 new file mode 100644 index 0000000..99d996d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeams.ps1 @@ -0,0 +1,41 @@ +function Get-ScrumTeams { +<# +.SYNOPSIS + Get the scrum teams per a document in Confluence (see: {function:Get-ScrumTeamsFromConfluence}) + +.PARAMETER Refresh + Force a refresh of the data. +#> + param ( + [Parameter(Mandatory = $false)] + [switch]$Refresh + ) + + $logLead = (Get-LogLeadName) + + # Check to see if the file exists in our normal cache location + # If the file doesn't exist, get the data from confluence, try to cache it, return the data + + $cacheFilePath = (Get-CacheFile 'ScrumTeams') + $cachedFileExists = (Test-Path $cacheFilePath) + if (!$Refresh -and $cachedFileExists) { + $cachedData = (Get-Content $cacheFilePath) + return $cachedData | ConvertFrom-Json | ConvertTo-Hashtable | ConvertTo-ScrumTeam + } else { + $scrumTeams = Get-ScrumTeamsFromConfluence + + try { + $scrumTeams | ConvertTo-Json -Depth 10 | Set-Content -Path $cacheFilePath -Force + } catch { + $errorObject = $PSItem + if (!(Test-IsUserLocalAdministrator)) { + Write-Host "$logLead : The cached data could not be saved. Is there a permissions concern?" + } else { + Write-Host "$logLead : The cached data could not be saved. Examine the error object for more details." + Write-ErrorObject $errorObject + } + } + + return $scrumTeams + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeamsFromConfluence.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeamsFromConfluence.ps1 new file mode 100644 index 0000000..eea7de5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ScrumTeamsFromConfluence.ps1 @@ -0,0 +1,117 @@ +function Get-ScrumTeamsFromConfluence { +<# +.SYNOPSIS + Get the list of scrum teams from the Alkami Confluence page that determines the teams. + Requires that the page has not changed. + +.LINK + https://confluence.alkami.com/display/AA/Current+Scrum+Teams +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $false, Position = 0)] + [string]$FullUrl = "https://confluence.alkami.com/display/AA/Current+Scrum+Teams", + [Parameter(Mandatory = $false, Position = 1)] + [string]$ArticleID, + [Parameter(Mandatory = $false, Position = 2)] + [PSCredential]$Credential + ) + + $logLead = (Get-LogLeadName) + + if ($null -eq $Credential) { + $Credential = (Get-CredentialFromEnvironmentVariables) + } + + if ($null -eq $Credential) { + Write-Error "$logLead : Can not talk to Jira without credentials. Returning." + return + } + + $headers = (Get-BasicAuthWebHeader -Credential $Credential) + $headers["Content-Type"] = "application/json" + + # left this data so I can separate the two things between "get random confluence page" and "get this specific page" + # I don't have a second page to read from _yet_, so I left this here for now + # Also works the same as the Add-JiraComment function, roughly. + # $url = (Get-ConfluenceBaseUrl) + # $baseUrl = "https://confluence.alkami.com/display/AA/Current+Scrum+Teams" + # Use this URL to ensure the ticket number as provided exists + # $confluencePageUrl = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/content/$ArticleID") + # do rest apis still use IWR for this particular function? + + $arguments = @{ + Headers = $headers + Uri = $FullUrl + Method = 'Get' + } + + try { + $response = Invoke-WebRequest @arguments + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-Host $arguments + Write-ErrorObject -ErrorItem $PSItem + Write-Error "$logLead : Could not connect to Confluence. Ensure proper credentials and try again, or verify the page hasn't moved." + return + } + + if ($null -eq $response) { + Write-Host "$logLead : Got no data from Confluence." + } + + $table = $null + try { + $table = $response.ParsedHtml.GetElementsByClassName("wrapped relative-table confluenceTable") + } catch [System.Runtime.InteropServices.COMException] { + Write-Host "$logLead : Not sure why the IE COM engine can't parse this table regularly. It appears to be a bug with an OOM condition.$([System.Environment]::NewLine)Reopen your terminal and try again I guess. Thanks COM." + return + } catch [System.NotSupportedException] { + Write-Host "$logLead : COM puked. No idea why. Reset your terminal I guess and try again?" + return + } + + if ($null -eq $table) { + Write-Host "$logLead : Could not parse the response data from Confluence. Raw data being returned." + return $response + } + + $headers = @() + $headersFound = $false + $tableData = @() + + foreach ($row in $table[0].tBodies[0].Rows) { + $team = @{} + foreach ($cell in $row.Cells) { + if (!$headersFound) { + $headers += $cell.innerText + } else { + $targetText = $cell.innerText + $anchors = @($cell.getElementsByTagName('a')) + if ($anchors.Count -gt 0) { + $href = $anchors[0].href + if ($href.IndexOf('jira.alkami.com') -gt -1) { + $targetText = $href + } + } + $team[$headers[$cellCounter]] = $targetText + } + $cellCounter += 1 + } + if ($headersFound) { + $tableData += $team + } + $headersFound = $true + $cellCounter = 0 + } + + if ($tableData.Count -eq 0) { + Write-Warning "$logLead : There were no rows somehow in the table object output. This is clearly a problem. Raw data being returned." + return $response + } + + $scrumTeams = (ConvertTo-ScrumTeam $tableData) + + return $scrumTeams +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-SemverHistory.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-SemverHistory.ps1 new file mode 100644 index 0000000..0fe3795 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-SemverHistory.ps1 @@ -0,0 +1,231 @@ +function Get-SemverHistory { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$Path = (Get-Location).Path + ) + + if ((Split-Path -Path $Path -Leaf) -eq 'sem.ver') { + # We have a semver file already + } else { + if (-not (Get-Item -Path $Path).PSIsContainer) { + throw "The Path argument should be a folder or a sem.ver specific file" + } + + $Path = Join-Path -Path $Path -ChildPath "sem.ver" + $Path = (Get-ChildItem -Path $Path -Recurse | Select-Object -First 1).FullName + } + + if (-not (Test-Path -Path $Path)) { + throw "Could not find the requested path [$Path]" + } + + $semverLog = git log -c -- $Path + + $commits = @() + $commit = $null + $inSemverLines = $false + foreach ($line in $semverLog) { + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + # This breaks if the comment starts with the word 'commit' on the left margin + if ($line.StartsWith("commit") -and [string]::IsNullOrWhiteSpace($Commit.SemverValue)) { + if ($null -ne $commit) { + $commits += $commit + } + $commit = @{ + Hash = ($line -split 'commit ')[1].Trim() + Author = "" + Date = "" + SemverValue = "" + SemverContents = @() + Comment = @() + } + $inSemverLines = $false + } elseif ($line.StartsWith("Author:")) { + $commit.Author = ($line -split 'Author:')[1].Trim() + } elseif ($line.StartsWith("Date:")) { + $commit.Date = ($line -split 'Date:')[1].Trim() + } else { + if ($line.StartsWith("diff --git")) { + # We don't care about [diff --git a/sem.ver b/sem.ver] type lines + continue + } elseif ($line.StartsWith("index") -and ($line -split ' ').Count -eq 3) { + # we don't care about [index 7c4d2b4..e1de4ac 100644] type lines + continue + } elseif ($line.StartsWith("---") -or $line.StartsWith("+++")) { + # we don't care about the diff filenames + continue + } elseif ($line.StartsWith("@@") -and $line.EndsWith("@@")) { + $inSemverLines = $true + continue + } elseif ($inSemverLines) { + if ($line.Trim().StartsWith("-") -or $line.StartsWith("\")) { + # ignore remove lines + continue + } elseif ($line.Trim().StartsWith("+")) { + # skip the first two characters + $line = $line.Substring(2) + } + + $commit.SemverContents += $line + } else { + $commit.Comment += $line.Trim() + } + } + } + # Save the last one too! + if ($null -ne $commit) { + $commits += $commit + } + + foreach ($commit in $commits) { + # Yes, it's a hack. It's cheaper than retrieving the file _at_ the hash tho + $jsonString = "$($commit.SemverContents -join '')".Trim() + $jsonString = $jsonString.Replace(' ', '').Replace("`t", "") + if ([string]::IsNullOrWhiteSpace($jsonString)) { + continue + } + if (($jsonString.IndexOf("Version") -eq -1) -or ($jsonString.StartsWith('{"Major"'))) { + $jsonString = "`"Version`": {$jsonString" + } + if (-not $jsonString.StartsWith("{")) { + $jsonString = "{$jsonString" + } + if ($jsonString.StartsWith("{{")) { + $jsonString = $jsonString.Substring(1) + $jsonString = "{`"Version`":$jsonString" + } + if (-not $jsonString.EndsWith("}")) { + $jsonString = "$jsonString}" + } + if (-not $jsonString.EndsWith("}}")) { + $jsonString = "$jsonString}" + } + $jsonString = $jsonString.Replace(' {{', '{') + $json = ConvertFrom-Json $jsonString + $commit.SemverValue = "$($json.Version.Major).$($json.Version.Minor).$($json.Version.Patch)" + $commit.SemverContents = $null + } + + return $commits +} + +function Get-GitHistory { + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'ByHash')] + [ValidateNotNullOrEmpty()] + [Alias('From')] + [string]$HashStart, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByHash')] + [ValidateNotNullOrEmpty()] + [Alias('To')] + [string]$HashEnd, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByObject')] + [Alias('FromEntry')] + [object]$StartEntry, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByObject')] + [Alias('ToEntry')] + [object]$EndEntry + ) + + $logLead = Get-LogLeadName + + if ($PSCmdlet.ParameterSetName -eq 'ByHash') { + if ($HashStart -eq $HashEnd) { + Write-Error "$logLead : You must supply two different hashes. These hashes were the same." + return + } + + $dateStart = Invoke-CallOperatorWithPathAndParameters git @('log', '--no-walk', '--format="%ai"', $HashStart) + $dateEnd = Invoke-CallOperatorWithPathAndParameters git @('log', '--no-walk', '--format="%ai"', $HashEnd) + + # Put them in the right order + if ($dateStart -lt $dateEnd) { + $HashStart, $HashEnd = $HashEnd, $HashStart + } + } else { + if ($StartEntry.Date -lt $EndEntry.Date) { + $StartEntry, $EndEntry = $EndEntry, $StartEntry + } + + $HashEnd = $EndEntry.Hash + $HashStart = $StartEntry.Hash + } + + $lines = Invoke-CallOperatorWithPathAndParameters git @('log', "$HashEnd...$HashStart", '--format="Author: %aN%nDate: %ai%n%B%n-="', '--notes') + + $results = @() + $result = $null + $skipAdd = $false + foreach ($line in $lines) { + if ($line.StartsWith("Author: ")) { + if ($null -ne $result) { + $results += $result + $skipAdd = $false + } + $result = @{ + Author = $line.Substring(8) + Date = $null + Comments = @() + JiraTickets = @() + } + } elseif ($line.Trim() -eq '-=') { + if ($skipAdd -eq $false) { + $results += $result + } + $skipAdd = $false + $result = $null + } elseif ($line.StartsWith("Date: ")) { + $result.Date = [DateTime]($line.Substring(6)) + } else { + $result.Comments += $line.Trim() + if ($line.StartsWith('Merge')) { + $skipAdd = $true + } + } + } + + foreach ($result in $results) { + $result.JiraTickets = (Select-String '[A-Z]+-[0-9]+' -InputObject $result.Comments -AllMatches -CaseSensitive).Matches.Value + } + + return $results +} + +function Get-RepoHistoryBySemver { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$Path = (Get-Location).Path + ) + + $results = @() + + Write-Progress -Activity "Getting history for semver" -PercentComplete 1 + $semverHistory = Get-SemverHistory -Path $Path + + for ($i = 0; $i -lt ($semverHistory.Count - 1); $i++) { + $from = $semverHistory[$i] + $to = $semverHistory[$i + 1] + Write-Progress -Activity "Getting history by semver" -PercentComplete ((100 * $i) / ($semverHistory.Count - 1)) -Status "Checking for data between $($from.Hash) and $($to.Hash)" + $commitHistory = Get-GitHistory -StartEntry $from -EndEntry $to + + $results += @{ + Version = $from.SemverValue + JiraTickets = ($commitHistory.JiraTickets | Sort-Object -Unique) + CommitHistory = $commitHistory + CommitCount = $commitHistory.Count + SemverData = $from + } + } + Write-Progress -Completed -Activity "Getting history by semver" + + return $results +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-ServersByPod.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-ServersByPod.ps1 new file mode 100644 index 0000000..ffb5a03 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-ServersByPod.ps1 @@ -0,0 +1,25 @@ +function Get-ServersByPod { + param ( + [Parameter(Mandatory = $false)] + [Alias('Environment')] + [ValidateScript({Assert-ValidAWSProfileName -ProfileName $_ -ArgumentValidationScript})] + $ProfileName = (Get-LocalCachedAWSProfile), + [Parameter(Mandatory = $true)] + [ArgumentCompleter( { + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $p = $fakeBoundParameters.ProfileName + if ([string]::IsNullOrWhiteSpace($p)) { + $p = (Get-LocalCachedAWSProfile) + } + $s = Get-CachedInstances -ProfileName $p + return $s.Designation | Sort-Object | Get-Unique + } )] + [string]$Designation + ) + + return (Get-CachedInstances -ProfileName $ProfileName).Where({$_.Designation -eq $Designation}) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-SiteTempDirectoryPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-SiteTempDirectoryPath.ps1 new file mode 100644 index 0000000..4b4f2bb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-SiteTempDirectoryPath.ps1 @@ -0,0 +1,103 @@ +function Get-SiteTempDirectoryPath { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [Alias('Site')] + [Alias('Name')] + [Alias('AppName')] + [string]$SiteOrAppName + ) + Start-Job -InputObject @{ SiteOrAppName = $SiteOrAppName; } -ScriptBlock { + Import-Module WebAdministration + $SiteOrAppName = $Input.SiteOrAppName + $hashCode = @" + public class HashCode + { + // https://stackoverflow.com/a/41575325/109749 ish + public static string GetDirectoryName(string id, string appname, string path) { + string v_app_name64b = HashCode.GetStringHashCode(("/LM/W3SVC/" + id + "/" + appname + path).ToLower(System.Globalization.CultureInfo.InvariantCulture)).ToString("x", System.Globalization.CultureInfo.InvariantCulture); + return HashCode.GetString64(v_app_name64b).ToString("x8") + "\\" + v_app_name64b; + } + + // System.Web.Util.StringUtil.GetStringHashCode() (System.Web.4.0.0.0) via System.Web.Hosting.ApplicationManager.CreateAppDomainWithHostingEnvironment(string appId, IApplicationHost appHost, HostingEnvironmentParameters hostingParameters) + internal unsafe static int GetStringHashCode(string s) + { + fixed(char* ptr = s){ + int num = 352654597; + int num2 = num; + int* ptr2 = (int*)ptr; + for (int i = s.Length; i > 0; i -= 4) + { + num = ((num << 5) + num + (num >> 27) ^ *ptr2); + if (i <= 2) + { + break; + } + num2 = ((num2 << 5) + num2 + (num2 >> 27) ^ ptr2[1]); + ptr2 += 2; + } + return num + num2 * 1566083941; + } + } + + // https://stackoverflow.com/a/41575325/109749 + internal static unsafe int GetString64(string s) + { + fixed (char* str = s) + { + int num3; + char* chPtr = str; + int num = 0x1505; + int num2 = num; + for (char* chPtr2 = chPtr; (num3 = chPtr2[0]) != '\0'; chPtr2 += 2) + { + num = ((num << 5) + num) ^ num3; + num3 = chPtr2[1]; + if (num3 == 0) + { + break; + } + num2 = ((num2 << 5) + num2) ^ num3; + } + return (num + (num2 * 0x5d588b65)); + } + } + } +"@; + + $cp = [System.CodeDom.Compiler.CompilerParameters]::new($assemblies); + $cp.CompilerOptions = '/unsafe'; + Add-Type -TypeDefinition $hashCode -CompilerParameters $cp; + + $rootOrAppName = "root"; + $physicalPath = ""; + $siteId = ""; + + $website = (Get-Website $siteOrAppName); + if ($null -ne $website) { + $physicalPath = $website.PhysicalPath.TrimEnd("\") + "\"; + $siteId = $website.id; + } else { + $appPool = (Get-WebApplication $siteOrAppName); + if ($null -eq $appPool) { + Write-Warning "could not match to any site or apppool. Did you provide the right information?" + exit -1; + } + $sitename = $appPool.GetParentElement().attributes["name"].Value; + $website = (Get-Website $sitename); + if ($null -ne $website) { + $physicalPath = $website.PhysicalPath.TrimEnd("\") + "\"; + $siteId = $website.id; + $rootOrAppName = $siteOrAppName + } else { + Write-Warning "Could not find the site for $siteOrAppName"; + exit -1; + } + } + + $tempFilePath = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\$rootOrAppName\" + [HashCode]::GetDirectoryName($siteid, $rootOrAppName, $physicalPath); + return $tempFilePath + } | Receive-Job -Wait -AutoRemoveJob +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-TenableAssetTag.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-TenableAssetTag.ps1 new file mode 100644 index 0000000..18da5bb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-TenableAssetTag.ps1 @@ -0,0 +1,30 @@ +function Get-TenableAssetTag { +<# +.SYNOPSIS + Get the tenable asset tag for current device +#> + [CmdletBinding()] + [OutputType([string])] + param( + ) + + if (Test-IsWindowsPlatform) { + return (Get-ItemProperty -Path HKLM:Software/Tenable/ -Name TAG).TAG + } + + if (Test-IsMacOSPlatform) { + try { + return (Get-Content /private/etc/tenable_tag) + } catch { + return (sudo cat /private/etc/tenable_tag) + } + } + + if (Test-IsLinuxPlatform) { + try { + return (Get-Content /etc/tenable_tag) + } catch { + return (sudo cat /etc/tenable_tag) + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-UpgradePackagesBetweenEnvironments.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-UpgradePackagesBetweenEnvironments.ps1 new file mode 100644 index 0000000..ed6d8d0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-UpgradePackagesBetweenEnvironments.ps1 @@ -0,0 +1,129 @@ +function Get-UpgradePackagesBetweenEnvironments { + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $false)] + [Alias('FromEnvironment')] + [ValidateScript({Assert-ValidAWSProfileName -ProfileName $_ -ArgumentValidationScript})] + $FromProfileName = (Get-LocalCachedAWSProfile), + [Parameter(Mandatory = $true)] + [ArgumentCompleter( { + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $p = $fakeBoundParameters.FromProfileName + if ([string]::IsNullOrWhiteSpace($p)) { + $p = (Get-LocalCachedAWSProfile) + } + $s = Get-CachedInstances -ProfileName $p + return $s.Designation | Sort-Object | Get-Unique + } )] + [string]$FromDesignation, + [Parameter(Mandatory = $false)] + [Alias('ToEnvironment')] + [ValidateScript({Assert-ValidAWSProfileName -ProfileName $_ -ArgumentValidationScript})] + $ToProfileName = (Get-LocalCachedAWSProfile), + [Parameter(Mandatory = $true)] + [ArgumentCompleter( { + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $p = $fakeBoundParameters.ToProfileName + if ([string]::IsNullOrWhiteSpace($p)) { + $p = (Get-LocalCachedAWSProfile) + } + $s = Get-CachedInstances -ProfileName $p + return $s.Designation | Sort-Object | Get-Unique + } )] + [string]$ToDesignation + + ) + + $dontReturn = $false + if ($PSCmdlet.MyInvocation.Line.IndexOf('=') -eq -1) { + $dontReturn = $true + } + + $packagesFrom = Invoke-JobRunner -JobInputs (Get-ServersByPod -ProfileName $FromProfileName -Designation $FromDesignation).Hostname -ReturnObjects -ScriptBlock { + param ($computerName) + + return (Invoke-Command -ComputerName $ComputerName -ScriptBlock { + $serverType = Get-ServerTypeByHostname -ComputerName (Get-FullyQualifiedServerName) + $nuspecs = Get-ChildItem -Path C:\ProgramData\chocolatey\lib\*.nuspec -Recurse + + $packageList = @() + foreach ($nuspec in $nuspecs) { + $xml = [xml](Get-Content -Path $nuspec.FullName -Raw) + $packageId = $xml.package.metadata.id + $version = $xml.package.metadata.version + $packageList += @{ ServerType = $serverType; PackageId = $packageId; Version = $version; } + } + return $packageList + }) + } + $packagesTo = Invoke-JobRunner -JobInputs (Get-ServersByPod -ProfileName $ToProfileName -Designation $ToDesignation).Hostname -ReturnObjects -ScriptBlock { + param ($computerName) + + return (Invoke-Command -ComputerName $ComputerName -ScriptBlock { + $serverType = Get-ServerTypeByHostname -ComputerName (Get-FullyQualifiedServerName) + $nuspecs = Get-ChildItem -Path C:\ProgramData\chocolatey\lib\*.nuspec -Recurse + + $packageList = @() + foreach ($nuspec in $nuspecs) { + $xml = [xml](Get-Content -Path $nuspec.FullName -Raw) + $packageId = $xml.package.metadata.id + $version = $xml.package.metadata.version + $packageList += @{ ServerType = $serverType; PackageId = $packageId; Version = $version; } + } + return $packageList + }) + } + + $packagesToIdsWeb = $packagesCI.Where({$_.ServerType -eq 'Web'}).PackageId | Sort-Object | Get-Unique + $packagesToIdsApp = $packagesCI.Where({$_.ServerType -ne 'Web'}).PackageId | Sort-Object | Get-Unique + + filter Remove-InfraPackages { + $knownInfra = @('7-Zip','Carbon','debugdiagnostic','DotNETFramework','filebeat','GoogleChrome','iiscrypto-cli','newrelic-dotnet','newrelic-infra','notepadplusplus','Pester','powershell-core','powershell-yaml','PsRedis','PSWindowsUpdate','ServicesPlus','WinDirStat') + if ($_.PackageId.StartsWith('Alkami.SRE') -or $_.PackageId.StartsWith('Alkami.DevOps') -or $_.PackageId.StartsWith('Alkami.Ops') -or $_.PackageId.StartsWith('Alkami.PowerShell') -or $_.PackageId.StartsWith('Alkami.Installer') -or $knownInfra -contains $_.PackageId) { + # + } else { + return $_ + } + } + + <# + $targetUpgradesWeb = @() + foreach ($package in ($packagesFrom | Remove-InfraPackages)) { + if ($package.ServerType -ne 'Web') { + continue + } + if ($packagesToIdsWeb -notcontains $package.PackageId) { + continue + } + $packagesTo.Where({$}) + } + #> + + $targetUpgradesWeb = $packagesFrom.Where({$_.ServerType -eq 'Web' -and $packagesToIdsWeb -contains $_.PackageId}) | Remove-InfraPackages | Foreach-Object { $package = $_; if ([Version]$packagesTo.Where({$_.PackageId -eq $package.PackageId}).Version -gt [Version]$package.Version) { "$($_.PackageId) $($_.Version)" } } | Sort-Object | Get-Unique + $targetUpgradesApp = $packagesFrom.Where({$_.ServerType -ne 'Web' -and $packagesToIdsApp -contains $_.PackageId}) | Remove-InfraPackages | Foreach-Object { $package = $_; if ([Version]$packagesTo.Where({$_.PackageId -eq $package.PackageId}).Version -gt [Version]$package.Version) { "$($_.PackageId) $($_.Version)" } } | Sort-Object | Get-Unique + + if (Test-IsCollectionNullOrEmpty -Collection $targetUpgradesWeb) { + $targetUpgradesWeb = @("No packages found in [$ToDesignation] on [$ToEnvironment] that are lower than [$FromDesignation] on [$FromEnvironment]") + } + + if (Test-IsCollectionNullOrEmpty -Collection $targetUpgradesApp) { + $targetUpgradesApp = @("No packages found in [$ToDesignation] on [$ToEnvironment] that are lower than [$FromDesignation] on [$FromEnvironment]") + } + + if ($dontReturn) { + $global:targetUpgradesApp = $targetUpgradesApp + $global:targetUpgradesWeb = $targetUpgradesWeb + } else { + return $targetUpgradesWeb, $targetUpgradesApp + } + +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-Uptime.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-Uptime.ps1 new file mode 100644 index 0000000..14d01b9 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-Uptime.ps1 @@ -0,0 +1,19 @@ +function Get-Uptime { + [CmdletBinding()] + [OutputType([string])] + param ( + ) + + if (Test-IsWindowsPlatform) { + $now = Get-Date + $lastStartTime = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime + + $uptime = $now - $lastStartTime + + return $uptime.ToString() + } else { + return "we clobbered the system uptime, doh" + } +} + +Set-Alias -Name uptime -Value Get-Uptime \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-UrlComponents.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-UrlComponents.ps1 new file mode 100644 index 0000000..052fabe --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-UrlComponents.ps1 @@ -0,0 +1,112 @@ +function Get-UrlComponents { +<# +.SYNOPSIS + Used to decompose a string as a url into the consituent components + +.PARAMETER Url + The parameter to be parsed +#> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [string]$Url + ) + + $logLead = (Get-LogLeadName) + + $originalString = $Url + $schemeDelimiter = "://" + $defaultPorts = @{ + ftp = 21; + ssh = 22; + telnet = 23; + mailto = 25; + http = 80; + ldap = 389; + https = 443; + "net.tcp" = 808; + } + + # Yes I could rely on hoisting but I don't like that. Harder to reason what's missing. + $username = $null + $password = $null + $Scheme = $null + $Hostname = $null + $Port = $null + $Credential = $null + $Query = $null + $Fragment = $null + + $schemeAt = $Url.IndexOf($schemeDelimiter) + if ($schemeAt -gt -1) { + $scheme = $Url.Substring(0,$schemeAt) + $Url = $Url.Substring($schemeAt + $schemeDelimiter.Length) + } + + $firstSlash = $Url.IndexOf('/') + $firstAt = $Url.IndexOf('@') + if ($firstAt -lt $firstSlash) { + # The first @ comes before the first slash, which indicates a user component + $Username = $Url.Substring(0, $firstAt) + $usernameSplit = $Username -split ':' + if ($usernameSplit.Length -gt 2) { + throw "$logLead : A username field can not contain more than two segments. You should probaly just not be using a username anyways, it's highly insecure. But if you must, check the RFC. Can only have one colon here. https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1" + } + $Username = $usernameSplit[0] + $password = $usernameSplit[1] + if (![string]::IsNullOrWhiteSpace($password)) { + $replacePassword = ":$password@" + $originalString = $originalString.Replace($replacePassword,":@") + # It is okay to force this here because we are literally parsing a raw text password. + $password = (ConvertTo-SecureString $password -AsPlainText -Force) + $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $Username, $password + # Either return the credential OR the Username, never both + $Username = $null + } + $Url = $Url.Substring($firstAt + 1) + } + + $firstSlash = $Url.IndexOf('/') + $Hostname = $Url.Substring(0, $firstSlash) + $Url = $Url.Substring($firstSlash) + + $portDelimiter = $Hostname.IndexOf(':') + if ($portDelimiter -gt -1) { + $Port = $Hostname.Substring($portDelimiter + 1) + $Hostname = $Hostname.Substring(0, $portDelimiter) + } + + $fragmentDelimiter = $Url.IndexOf('#') + if ($fragmentDelimiter -gt -1) { + $Fragment = $Url.Substring($fragmentDelimiter) + $Url = $Url.Substring(0, $fragmentDelimiter) + } + + $queryDelimiter = $Url.IndexOf('?') + if ($queryDelimiter -gt -1) { + $Query = $Url.Substring($queryDelimiter) + $Url = $Url.Substring(0, $queryDelimiter) + } + + # We have trimmed off the scheme, user, host, port, query, fragment. All that is left is the path. + $Path = $Url + $segments = $null + if (![string]::IsNullOrWhiteSpace($Path)) { + $segments = @($Path -split '/').Where({![string]::IsNullOrWhiteSpace($_)}) + } + + return New-Object PSCustomObject -Property @{ + Hostname = $Hostname; + Scheme = $Scheme; + Port = $Port; + Path = $Path; + Segments = $segments; + Query = $Query; + Fragment = $Fragment; + Username = $Username; + Credential = $Credential; + OriginalString = $originalString; + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Get-WindowsThreadSliceTime.ps1 b/Modules/Cole.PowerShell.Developer/Public/Get-WindowsThreadSliceTime.ps1 new file mode 100644 index 0000000..0b63268 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Get-WindowsThreadSliceTime.ps1 @@ -0,0 +1,24 @@ +function Get-WindowsThreadSliceTime { +<# +.SYNOPSIS + Used to determine the windows thread slice time. Useful for Invoke-JobRunner +#> + [CmdletBinding(DefaultParameterSetName = 'Milliseconds')] + param ( + [Parameter(ParameterSetName = "Milliseconds")] + [switch]$Milliseconds, + + [Parameter(ParameterSetName = "Ticks")] + [switch]$Ticks + ) + Add-Type -Assembly "System.ServiceModel.Internals" + $assembly = [System.AppDomain]::CurrentDomain.GetAssemblies().Where({ $_.GetName().Name -match 'System.ServiceModel.Internals' }) + $timer = $assembly.GetTypes().Where({ $_.Name -eq 'IOThreadTimer' }) + $timerValue = $timer.GetProperty("SystemTimeResolutionTicks").GetValue($null) + + if ($Milliseconds) { + return $timerValue / 10000 + } else { + return $timerValue + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Grant-AclOnCert.ps1 b/Modules/Cole.PowerShell.Developer/Public/Grant-AclOnCert.ps1 new file mode 100644 index 0000000..faea5ea --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Grant-AclOnCert.ps1 @@ -0,0 +1,85 @@ +function Grant-AclOnCert { +<# +.SYNOPSIS + Set the ACL on a certificate by thumbprint + +.PARAMETER Thumbprint + The certificate thumbprint to apply permissions to + +.PARAMETER FriendlyName + The certificate friendly name to apply permissions to + +.PARAMETER Identity + The user or group to apply privileges to + +.PARAMETER FileSystemRights + What rights are being granted + +.PARAMETER AccessControlType + AccessControlType of permission to apply. Usually "Allow" + +.PARAMETER StoreName + The store where the certificate is found. Most commonly used is 'My' +#> + [CmdletBinding(DefaultParameterSetName = 'Thumbprint')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Thumbprint', Position = 0)] + [psobject]$Thumbprint, + [Parameter(Mandatory = $true, ParameterSetName = 'FriendlyName', Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$FriendlyName, + [Parameter(Mandatory = $true)] + [string]$Identity, + [Parameter(Mandatory = $true)] + [System.Security.AccessControl.FileSystemRights]$FileSystemRights, + [Parameter(Mandatory = $false)] + [string]$AccessControlType = "Allow", + [Parameter(Mandatory = $false)] + [string]$StoreName = "My" + ) + + $logLead = Get-LogLeadName + + $certs = @() + + if ($PSCmdlet.ParameterSetName -eq 'Thumbprint') { + $certs += Get-Item -Path cert:\LocalMachine\$StoreName\$Thumbprint + } + + if ($PSCmdlet.ParameterSetName -eq 'FriendlyName') { + $certs += (Get-ChildItem -Path cert:\LocalMachine\$StoreName\).Where({$_.FriendlyName -eq $FriendlyName}) + } + + if ($certs.Count -eq 0) { + if ($PSCmdlet.ParameterSetName -eq 'Thumbprint') { + Write-Warning "$logLead : No certificate found at path [cert:\LocalMachine\$StoreName\$Thumbprint]" + } + if ($PSCmdlet.ParameterSetName -eq 'FriendlyName') { + Write-Warning "$logLead : No certificate found at path [cert:\LocalMachine\$StoreName] with FriendlyName [$FriendlyName]" + } + return + } + + # This is the known location where these are stored + $keyPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + + foreach ($cert in $certs) { + # This is a magic value + $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + + # Get the full path of where the file should exist + $keyFullPath = $keyPath + $keyName + + if ([string]::IsNullOrWhiteSpace($keyName)) { + Write-Warning "$logLead : Either the file does not exist at [$keyFullPath] or you don't have permission to get details about this file." + return + } + + # Get the ACL object so we can add stuff to it + $acl = (Get-Item $keyFullPath).GetAccessControl("Access") + $permission = $Identity,$FileSystemRights,$AccessControlType + $accessRule = New-Object -Type System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + $acl.AddAccessRule($accessRule) + Set-Acl -Path $keyFullPath -AclObject $acl + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Group-Numbers.ps1 b/Modules/Cole.PowerShell.Developer/Public/Group-Numbers.ps1 new file mode 100644 index 0000000..ea55624 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Group-Numbers.ps1 @@ -0,0 +1,34 @@ +function Group-Numbers { +<# +.SYNOPSIS + Group an array of numbers into islands of contiguous sequences +#> + [CmdletBinding()] + [OutputType([System.Collections.ArrayList])] + param ( + [Parameter(Mandatory = $true)] + [int[]]$Values + ) + + $groups = (New-Object -TypeName "System.Collections.ArrayList") + + if ($Values.Length -eq 0) { + return $groups + } + + $len = $Values.Length + $group = (New-Object -TypeName "System.Collections.ArrayList") + $group.Add($Values[0]) | Out-Null + for($i = 1; $i -lt $len; $i++) { + if (($Values[$i] - $Values[$i-1]) -eq 1) { + $group.Add($Values[$i]) | Out-Null + } else { + $groups.Add($group) | Out-Null + $group = (New-Object -TypeName "System.Collections.ArrayList") + $group.Add($Values[$i]) | Out-Null + } + } + $groups.Add($group) | Out-Null + + return $groups +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Import-AlkamiConsoleHost.ps1 b/Modules/Cole.PowerShell.Developer/Public/Import-AlkamiConsoleHost.ps1 new file mode 100644 index 0000000..f7abe45 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Import-AlkamiConsoleHost.ps1 @@ -0,0 +1,263 @@ +function Import-AlkamiConsoleHost { + Add-Type -TypeDefinition @" +namespace Alkami { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Management.Automation.Internal; + using System.Management.Automation.Internal.Host; + using System.Management.Automation.Runspaces; + using System.Security; + using System.Text; + using Microsoft.PowerShell; + using Microsoft.Win32; + using Microsoft.Win32.SafeHandles; + + public class AlkamiDefaultHost : PSHost { + public AlkamiDefaultHost(CultureInfo currentCulture, CultureInfo currentUICulture) + { + _name = "Alkami Default Host"; + _instanceId = Guid.NewGuid(); + _currentCulture = currentCulture; + _currentUICulture = currentUICulture; + _ui = new AlkamiConsoleHostUserInterface(); + _version = new Version(1,0,0); + } + + private string _name; + public override string Name { get { return _name; } } + private Version _version; + public override Version Version { get { return _version; } } + private Guid _instanceId; + public override Guid InstanceId { get { return _instanceId; } } + private PSHostUserInterface _ui; + public override PSHostUserInterface UI { get { return _ui; } } + private CultureInfo _currentCulture; + public override CultureInfo CurrentCulture { get { return _currentCulture; } } + private CultureInfo _currentUICulture; + public override CultureInfo CurrentUICulture { get { return _currentUICulture; } } + + public override void SetShouldExit(int exitCode) { } + public override void EnterNestedPrompt() { } + public override void ExitNestedPrompt() { } + public override void NotifyBeginApplication() { } + public override void NotifyEndApplication() { } + } + + public class MessageEntry { + public MessageEntry() { + TimeStamp = DateTime.Now; + Exception = null; + } + + public DateTime TimeStamp { get; set; } + public string Message { get; set; } + public string MessageType { get; set; } + public ConsoleColor ForegroundColor { get; set; } + public ConsoleColor BackgroundColor { get; set; } + public object MessageData { get; set; } + public Exception Exception { get; set; } + + public static MessageEntry Information(string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = ConsoleColor.DarkCyan; + messageEntry.ForegroundColor = ConsoleColor.White; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Information"; + return messageEntry; + } + + public static MessageEntry Information(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Information"; + return messageEntry; + } + + public static MessageEntry Verbose(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Verbose"; + return messageEntry; + } + + public static MessageEntry Debug(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Debug"; + return messageEntry; + } + + public static MessageEntry Warning(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Warning"; + return messageEntry; + } + + public static MessageEntry Error(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Error"; + return messageEntry; + } + + public static MessageEntry Error(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, Exception exception, object messageData = null) { + var messageEntry = new MessageEntry(); + messageEntry.BackgroundColor = backgroundColor; + messageEntry.ForegroundColor = foregroundColor; + messageEntry.MessageData = messageData; + messageEntry.Message = value; + messageEntry.MessageType = "Error"; + messageEntry.Exception = exception; + return messageEntry; + } + } + + public class AlkamiConsoleHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection + { + private List MessageList = new List(); + + public override bool SupportsVirtualTerminal { get { return false; } } + + public override PSHostRawUserInterface RawUI { get { return null; } } + + public ConsoleColor ErrorForegroundColor { get; set; } + public ConsoleColor ErrorBackgroundColor { get; set; } + + public ConsoleColor WarningForegroundColor { get; set; } + public ConsoleColor WarningBackgroundColor { get; set; } + + public ConsoleColor DebugForegroundColor { get; set; } + public ConsoleColor DebugBackgroundColor { get; set; } + + public ConsoleColor VerboseForegroundColor { get; set; } + public ConsoleColor VerboseBackgroundColor { get; set; } + + public ConsoleColor ProgressForegroundColor { get; set; } + public ConsoleColor ProgressBackgroundColor { get; set; } + + public AlkamiConsoleHostUserInterface() + { + var defaultBackgroundColor = ConsoleColor.DarkBlue; + + this.ProgressBackgroundColor = ConsoleColor.DarkCyan; + this.ProgressForegroundColor = ConsoleColor.Yellow; + + this.VerboseBackgroundColor = defaultBackgroundColor; + this.VerboseForegroundColor = ConsoleColor.Yellow; + + this.DebugBackgroundColor = ConsoleColor.DarkCyan; + this.DebugForegroundColor = ConsoleColor.Yellow; + + this.WarningBackgroundColor = defaultBackgroundColor; + this.WarningForegroundColor = ConsoleColor.Yellow; + + this.ErrorBackgroundColor = ConsoleColor.Black; + this.ErrorForegroundColor = ConsoleColor.Red; + } + + public override string ReadLine() + { + return ""; + } + + public override SecureString ReadLineAsSecureString() + { + return null; + } + + public override void Write(string value) + { + MessageList.Add(MessageEntry.Information(value)); + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + var message = MessageEntry.Information(foregroundColor, backgroundColor, value); + MessageList.Add(message); + } + + public override void WriteLine(string value) + { + MessageList.Add(MessageEntry.Information(value)); + } + + public override void WriteDebugLine(string message) + { + MessageList.Add(MessageEntry.Debug(DebugForegroundColor, DebugBackgroundColor, message)); + } + + public override void WriteInformation(InformationRecord record) + { + MessageList.Add(MessageEntry.Information("InformationRecord", record)); + } + + public override void WriteVerboseLine(string message) + { + MessageList.Add(MessageEntry.Verbose(VerboseForegroundColor, VerboseBackgroundColor, message)); + } + + public override void WriteWarningLine(string message) + { + MessageList.Add(MessageEntry.Warning(WarningForegroundColor, WarningBackgroundColor, message)); + } + + public override void WriteProgress(long sourceId, ProgressRecord record) { } + + public override void WriteErrorLine(string value) + { + MessageList.Add(MessageEntry.Error(ErrorForegroundColor, ErrorBackgroundColor, value)); + } + + public override Dictionary Prompt(string caption, string message, Collection descriptions) + { + return null; + } + + public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) + { + return 0; + } + + public Collection PromptForChoice(string caption, string message, Collection choices, IEnumerable defaultChoices) + { + return null; + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + return null; + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + return null; + } + } +} +"@; +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Import-FigletFontFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/Import-FigletFontFile.ps1 new file mode 100644 index 0000000..d186cb8 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Import-FigletFontFile.ps1 @@ -0,0 +1,36 @@ +function Import-FigletFontFile { + [CmdletBinding(DefaultParameterSetName = 'Name')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [Alias('Filename')] + [Alias('Name')] + $FontName, + [Parameter(Mandatory = $false, ParameterSetName = 'Name')] + [Alias('Parent')] + [Alias('Folder')] + $FontFolder, + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + $Path + ) + + $logLead = Get-LogLeadName + + if ($PSCmdlet.ParameterSetName -eq 'Name') { + if ([string]::IsNullOrWhiteSpace($FontFolder)) { + $FontFolder = Get-FigletFontFolder + } + + if ([string]::IsNullOrWhiteSpace($FontFolder)) { + Write-Error "$logLead : Can not find figlet fonts folder" + return + } + + $Path = Join-Path -Path $FontFolder -ChildPath "$FontName.flf" + } + + if (!(Test-Path -Path $Path)) { + Write-Error "$logLead : Font file not found. Font may not be installed. New files may be manually installed to module folder but will be lost when module is updated." + } + + $lines = (Get-Content -Path $Path) +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Initialize-AWSCredentials.ps1 b/Modules/Cole.PowerShell.Developer/Public/Initialize-AWSCredentials.ps1 new file mode 100644 index 0000000..7f135d5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Initialize-AWSCredentials.ps1 @@ -0,0 +1,48 @@ +function Initialize-AWSCredentials { +<# +.SYNOPSIS + Use this to initialize the AWS Credentials file on your machine +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + $Username = "$($env:username)-cli", + [Parameter(Mandatory = $false)] + $CredentialsFilePath = '~/.aws/credentials', + [Parameter(Mandatory = $false)] + $ConfigFilePath = '~/.aws/credentials' + ) + + $logLead = (Get-LogLeadName) + + if (Test-Path $CredentialsFilePath) { + throw "$logLead : The file at [$CredentialsFilePath] already exists. Not going to reinitialize." + } + + if (Test-Path $ConfigFilePath) { + throw "$logLead : The file at [$ConfigFilePath] already exists. Not going to reinitialize." + } + + Write-Host "$logLead : Proceeding with username [$Username]." + + $qrPngPath = (Expand-Path '~/Desktop/AWS_MFA_QR.png') + + $newIdentityRaw = (aws iam create-virtual-mfa-device --virtual-mfa-device-name $Username --outfile $qrPngPath --bootstrap-method QRCodePNG --no-verify-ssl) + $newIdentity = (ConvertFrom-Json ($newIdentityRaw | Out-String)) + $virtualMFADeviceSerialNumber = $newIdentity.VirtualMFADevice.SerialNumber + + Start-Process $qrPngPath + + # get input #1 + $code1 = Read-Host "Please enter the first MFA device generated value" + # get input #2 + $code2 = Read-Host "Please enter the second MFA device generated value" + + (aws iam enable-mfa-device --user-name $Username --serial $virtualMFADeviceSerialNumber --authentication-code1 $code1 --authentication-code2 $code2 --no-verify-ssl) + + $RoleName = (Get-AWSConfigRoleNameForUser) + + New-AWSCredentialsFile -FilePath $CredentialsFilePath + New-AWSConfigFile -FilePath $ConfigFilePath -virtualMFADeviceSerialNumber $virtualMFADeviceSerialNumber -RoleName $RoleName +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiDeveloperSQLServer.ps1 b/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiDeveloperSQLServer.ps1 new file mode 100644 index 0000000..5b0982b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiDeveloperSQLServer.ps1 @@ -0,0 +1,103 @@ +function Install-AlkamiDeveloperSQLServer { +<# +.SYNOPSIS + This script is used to help consistently install SQL server for developers against the Alkami Platform + +.PARAMETER SetupExePath + The path to the setup.exe program for SQL Server. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $SetupExePath + ) + + if ((Split-Path $SetupExePath -Leaf -ErrorAction Continue) -ne "setup.exe") { + $SetupExePath = (Join-Path $SetupExePath "setup.exe") + } + + if (!(Test-Path $SetupExePath -ErrorAction Continue)) { + throw 'Can not find the setup.exe path' + } + + $user = "$env:UserDomain\$env:USERNAME" + Write-Host "Setting [$user] as local sql sa admin" + + $securePassword = [System.Web.Security.Membership]::GeneratePassword(24,5).Replace('"',"'") + + $configFileContents = @" +[OPTIONS] +IACCEPTSQLSERVERLICENSETERMS="True" +IACCEPTPYTHONLICENSETERMS="True" +ACTION="Install" +IACCEPTROPENLICENSETERMS="True" +SUPPRESSPRIVACYSTATEMENTNOTICE="True" +ENU="True" +QUIET="True" +QUIETSIMPLE="False" +UpdateEnabled="False" +USEMICROSOFTUPDATE="False" +SUPPRESSPAIDEDITIONNOTICE="True" +UpdateSource="MU" +FEATURES=SQLENGINE,CONN,SDK +HELP="False" +INDICATEPROGRESS="True" +X86="False" +INSTANCENAME="MSSQLSERVER" +INSTALLSHAREDDIR="C:\Program Files\Microsoft SQL Server" +INSTALLSHAREDWOWDIR="C:\Program Files (x86)\Microsoft SQL Server" +INSTANCEID="MSSQLSERVER" +SQLTELSVCACCT="NT Service\SQLTELEMETRY" +SQLTELSVCSTARTUPTYPE="Automatic" +INSTANCEDIR="C:\Program Files\Microsoft SQL Server" +AGTSVCACCOUNT="NT Service\SQLSERVERAGENT" +AGTSVCSTARTUPTYPE="Manual" +COMMFABRICPORT="0" +COMMFABRICNETWORKLEVEL="0" +COMMFABRICENCRYPTION="0" +MATRIXCMBRICKCOMMPORT="0" +SQLSVCSTARTUPTYPE="Automatic" +FILESTREAMLEVEL="0" +SQLMAXDOP="8" +ENABLERANU="False" +SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS" +SQLSVCACCOUNT="NT Service\MSSQLSERVER" +SQLSVCINSTANTFILEINIT="False" +SQLSYSADMINACCOUNTS="$user" +SECURITYMODE="SQL" +SQLTEMPDBFILECOUNT="8" +SQLTEMPDBFILESIZE="8" +SQLTEMPDBFILEGROWTH="64" +SQLTEMPDBLOGFILESIZE="8" +SQLTEMPDBLOGFILEGROWTH="64" +ADDCURRENTUSERASSQLADMIN="False" +TCPENABLED="1" +NPENABLED="1" +BROWSERSVCSTARTUPTYPE="Automatic" +SQLMAXMEMORY="2147483647" +SQLMINMEMORY="0" +SAPWD="$securePassword" +"@ + + $installTempConfigPath = (New-TemporaryFile) + Set-Content -Path $installTempConfigPath -Value $configFileContents + $errorOutputFile = (Join-Path (Split-Path $installTempConfigPath -Parent) "errorOutput.txt") + $standardOutputFile = (Join-Path (Split-Path $installTempConfigPath -Parent) "standardOutput.txt") + + Write-Host "Starting the install of SQL Server" + Start-Process $SetupExePath "/ConfigurationFile=$installTempConfigPath" -Wait -RedirectStandardOutput $standardOutputFile -RedirectStandardError $errorOutputFile + + $newTempFile = (New-TemporaryFile); Set-Content -value "Please record this secure password somewhere for future reference.`r`nThis password will not be available when you close this window.`r`nThis is the SQL Server root sa password and is very important.`r`n`r`n SA PASSWORD: $securePassword`r`n`r`nPlease maintain this password in a secure location." -Path $newTempFile; notepad.exe $newTempFile; Start-Sleep -Seconds 10; Remove-Item $newTempFile; + + Remove-Item $installTempConfigPath -Force + + $standardOutput = Get-Content $standardOutputFile -Delimiter "\r\n" + + Write-Host $standardOutput + + $errorOutput = Get-Content $errorOutputFile -Delimiter "\r\n" + + Write-Host $errorOutput + + Write-Host "If no red text then SQL Server Successfully Installed!" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiPackage.ps1 b/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiPackage.ps1 new file mode 100644 index 0000000..13173bd --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Install-AlkamiPackage.ps1 @@ -0,0 +1,155 @@ +function Install-AlkamiPackage { +<# +.SYNOPSIS + Install-AlkamiPackage is a replacement for chocolatey that can do the relevant 90% of what Chocolatey does that Alkamists typically rely on on a regular basis. + +.DESCRIPTION + This is not a 1:1 replacement for chocolatey nor should it be considered as such. + + If packages have an AlkamiManifest, then no included scripts will be run even when RunScripts is present. + Instead the expected path for a given manifested package will be executed natively. + + The converse, however, will hold. If IgnoreScripts is specified, even the expected manifested package install will not occur. + +.PARAMETER PackageId + This is a space, comma, or semicolon delimited list of packages to be installed. The specified version will apply to all packages in this list. + See alternatively: -FormattedPackageList + +.PARAMETER PackageVersion + This is the version that will be applied to all packages to be installed. When absent will default to the latest available package. + +.PARAMETER Latest + Defaults to true. When the version is not specified in PackageVersion, will attempt to find the latest package version. + When set to false, and no PackageVersion is provided, will error. + +.PARAMETER Feed + This is where to find the package(s). When not specified, will check all the feeds listed in the naive chocolatey feed folder, as well as a global feed cache at each of the following locations: (cumulatively merged) + * (Get-ProgramDataPath)\Alkami\Installer\feeds.config (xml) + * (Get-ProgramDataPath)\Alkami\Installer\feeds.json + * \.Alkami\Installer\feeds.config (xml) + * \.Alkami\Installer\feeds.json + The reason for multiple feed files is to allow for user and machine specification, as well as managed (xml) versus locally configured (json) global values. + +.PARAMETER RunScripts + Run included scripts (when package does not contain an AlkamiManifest at the root, this will execute Alkami code paths for manifested packages) + Note: No "chocolatey provided" functions exist. This is intentional. + To set this value globally, use the next line, substituting Machine for User as appropriately desired + Set-EnvironmentVariable -Key 'ALKAMI__InstallPackages__AlwaysRunScripts' -Value 'true' -StoreName User + +.PARAMETER IgnoreScripts + Do not run any included scripts. Will not execute Alkami code paths. + To set this value globally, use the next line, substituting Machine for User as appropriately desired + Set-EnvironmentVariable -Key 'ALKAMI__InstallPackages__AlwaysRunScripts' -Value 'false' -StoreName User + +.PARAMETER IgnoreDependencies + Will not try to resolve package dependencies. Defaults to false (will attempt to download and install dependency packages - obeys the RunScripts/IngoreScripts flag) + This flag is overridden by ForceDependencies. + To set this value globally, use the next line, substituting Machine for User as appropriately desired + Set-EnvironmentVariable -Key 'ALKAMI__InstallPackages__IgnoreDependencies' -Value 'true' -StoreName User + +.PARAMETER ForceDependencies + Will try to resolve package dependencies. Will honor the RunScripts/IgnoreScripts flag as appropriate. + This flag overrides IgnoreDependencies. + To set this value globally, use the next line, substituting Machine for User as appropriately desired + Set-EnvironmentVariable -Key 'ALKAMI__InstallPackages__ForceDependencies' -Value 'true' -StoreName User + +.PARAMETER NoProgress + This switch will hide output on progression in scenarios where a visual guide would be presented. + +.PARAMETER StopOnFirstFailure + Will attempt to stop on first failure execution. Not all task segments support stopping on first failure. + +.PARAMETER LimitOutput + Will reduce the number of output lines where possible. Some output may be unpreventable. + +.PARAMETER Timeout + Per package process timeout. Not adhered to for all action steps. + +.PARAMETER Force + When preesented with a yes/no scenario, will always choose yes, even if destructive. + This switch is overridden by WhatIf. + +.PARAMETER WhatIf + When preseneted with a yes/no scenario, will always choose no. Attempts to not alter system state. + This switch overrides Force. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromRemainingArguments = $true)] + [Alias('Name')] + [Alias('Id')] + [string[]]$PackageId, + [Alias('Version')] + [Alias('v')] + [string]$PackageVersion, + [string[]]$FormattedPackageList, + [switch]$Latest = $true, + [Parameter()] + [Alias('Source')] + [Alias('s')] + [Alias('FeedUrl')] + [string]$Feed, + [Alias('y')] + [Alias('confirm')] + [Alias('yes')] + [switch]$RunScripts, + [Alias('n')] + [Alias('no')] + [Alias('SkipScripts')] + [switch]$IgnoreScripts, + [Parameter()] + [switch]$NoProgress, + [Parameter()] + [Alias('pre')] + [Alias('usePre')] + [Alias('prerelease')] + [switch]$AllowPrereleasePackages, + [Parameter()] + [Alias('i')] + [switch]$IgnoreDependencies, + [Parameter()] + [Alias('x')] + [switch]$ForceDependencies, + [switch]$StopOnFirstFailure, + [Parameter()] + [Alias('r')] + [switch]$LimitOutput, + [Parameter()] + [int]$Timeout = 2700, + [Parameter()] + [switch]$Force, + [Parameter()] + [switch]$WhatIf + ) + + $logLead = Get-LogLeadName + + Write-Host $PackageId + + if ($WhatIf) { + Write-Host "!! WHATIF MODE ENABLED !!`n" + if ($Force) { + Write-Host "$logLead : WhatIf mode enabled, disabling -Force flag" + } + } + + if (-not $Latest -and [string]::IsNullOrWhiteSpace($PackageVersion)) { + throw "$logLead : Must specify a PackageVersion when specifying Latest:`$false" + } + + if (-not $RunScripts -and -not $IgnoreScripts) { + $runscriptEnvVarName = "ALKAMI__InstallPackages__AlwaysRunScripts" + $existingVar = Get-EnvironmentVariable -Key $runscriptEnvVarName + $RunScripts = $existingVar -eq 'true' + $IgnoreScripts = $existingVar -ne 'true' + if (!$LimitOutput) { + if ([string]::IsNullOrWhiteSpace($existingVar)) { + Write-Host "$logLead : Neither RunScripts nor IgnoreScripts were specified. Will default to IgnoreScripts. If you want to enable running scripts by default for the future, please run the following command`n`n`tSet-EnvironmentVariable -Key $runscriptEnvVarName -Value 'true' -StoreName User`n`nYou can set the store to Machine as well if you want it to apply to all users, or set the global value to false, etc" + } + } + } + + if ($AllowPrereleasePackages -and -not [string]::IsNullOrWhiteSpace($PackageVersion)) { + Write-Warning "$logLead : Can not specify version and ask for allowing prerelease versions to be installed. Please adjust input in the future." + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Build.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Build.ps1 new file mode 100644 index 0000000..7937883 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Build.ps1 @@ -0,0 +1,51 @@ +function Invoke-Build { +<# +.SYNOPSIS + Run the build command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "build.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No build path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name build -Value Invoke-Build -Force +<# +New-Alias build .\build.ps1 + +New-Alias db .\db.ps1 + +New-Alias test .\test.ps1 + +New-Alias hosts .\hosts.ps1 + +New-Alias install .\install.ps1 + +New-Alias deploy .\deploy.ps1 +#> \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-CallOperatorWithPathAndParameters.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-CallOperatorWithPathAndParameters.ps1 new file mode 100644 index 0000000..3fbeb4c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-CallOperatorWithPathAndParameters.ps1 @@ -0,0 +1,29 @@ +function Invoke-CallOperatorWithPathAndParameters { +<# +.SYNOPSIS + This function is basically just a wrapper for unit-testing purposes. + Used to call a system file with an array of string args. + The best use-case for this is sc.exe execution, or topshelf installers. + +.PARAMETER Path + The file path being invoked + +.PARAMETER Arguments + This value just gets splatted as passed in. + +.OUTPUTS + This function outputs whatever came out of the called function +#> + [CmdletBinding()] + [OutputType([System.Object])] + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [Parameter(Mandatory=$true)] + [string[]]$Arguments + ) + + # Fun fact: $LASTEXITCODE is set by the past .exe to run in the call stack, not by functions + # So if this sets a $LASTEXITCODE we can test for it in the caller function + (& $Path @Arguments) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeAllProjects.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeAllProjects.ps1 new file mode 100644 index 0000000..bbcf66a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeAllProjects.ps1 @@ -0,0 +1,225 @@ +function Invoke-CategorizeAllProjects { + <# +.SYNOPSIS + Categorize each and every solution in the Alkami repository infrastructure for package componentization structure. + Recurses the current path or specified location for all *.sln + +.PARAMETER Path + The location to start recursing from + +.PARAMETER OutputPath + The location to write the final values to +#> + + <# +foreach sln +foreach csproj +if (has manifest) + isX/Y/Z + break +if (inherits X,Y,Z) + isX/Y/Z + break +if (package contains X,Y,Z) + isX/Y/Z + break +#> + + <# +Providers - Look for packages.config Alkami.Common and ProcessorBase or ConnectorBase +Web Target - Look for MVC ref in packages.config +Widgets - Look for SiteText xml +Widgets - Look for AreaRegistration +WebExtensions - Look for IAlkamiWebExtension or IAlkamiModule +WebApplications - Look for global.asax + +Services are already taken care of +but I still need a way to deref walk the tree for providers and services to get their provider_type and provider_assembly_info and provider_name +#> + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $false)] + [string]$Path = (Get-RepoCheckpointPath), + [Parameter(Mandatory = $false)] + [string]$OutputPath + ) + + $psStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + $logLead = (Get-LogLeadName) + + if (!(Test-Path -Path $Path)) { + $Path = (Get-Location) + } + + $outputPathSpecified = ![string]::IsNullOrWhiteSpace($OutputPath) + + $solutions = @() + + $allSlns = (Get-ChildItem -Path (Join-Path -Path $Path -ChildPath '*.sln') -Recurse).FullName + + $solutions = Invoke-JobRunner -JobInputs $allSlns -ReturnObjects -ScriptBlock { + param ($solutionPath) + + $obsoleteRegex = "^(zz|deprecated|archive|obsolete|xxx)" + + if ($solutionPath.IndexOf('[') -gt -1) { + Write-Host "$logLead : Not even trying to touch [$solutionPath] due to bad name format" + } + try { + $pathSplits = ($solutionPath -split '\\') + $solutionRepo = $pathSplits[2] + $solutionFolderPath = (Split-Path -Path $solutionPath -Parent) + $solutionName = (Split-Path -Path $solutionFolderPath -Leaf) + $solutionProject = (Split-Path -Path (Split-Path -Path $solutionFolderPath -Parent) -Leaf) + $solutionFileObject = (ConvertFrom-SlnFile -Path $solutionPath) + $files = (Get-ChildItem -Path $solutionFolderPath) + $semVerPath = ($files.Where({ $_.Name -eq "sem.ver" }).FullName) + $solutionHasSemVer = $null + $solutionHasPackagesConfig = $null + $semverValueRaw = $null + $semverValueVersion = $null + if (![string]::IsNullOrWhiteSpace($semVerPath)) { + $solutionHasSemVer = $true + try { + $semverValueRaw = (ConvertFrom-Json (Get-Content -Path $semVerPath -Raw)) + if (($null -ne $semverValueRaw) -and ($null -ne $semverValueRaw.Version)) { + $semverValueVersion = "$($semverValueRaw.Version.Major).$($semverValueRaw.Version.Minor).$($semverValueRaw.Version.Patch)" + } + } catch { + Write-Warning "$logLead : Could not capture the semver from [$semverPath]" + } + } + $packagesConfigPath = ($files.Where({ $_.Name -eq "packages.config" }).FullName) + $packagesConfig = $null + if (![string]::IsNullOrWhiteSpace($packagesConfigPath)) { + $solutionHasPackagesConfig = $true + $packagesConfig = (([xml](Get-Content $packagesConfigPath)).packages.package | Select-Object -Property id, version, targetFramework) + } + $nuspecPath = @($files.Where({ $_.Name.EndsWith(".nuspec") }).FullName)[0] + $nuspecContent = $null + $nuspecIsChocolatey = $null + $solutionHasNuspec = $null + $solutionNuspecId = $null + if (![string]::IsNullOrWhiteSpace($nuspecPath)) { + $solutionHasNuspec = $true + $nuspecContent = [xml](Get-Content $nuspecPath) + $solutionNuspecId = $nuspecContent.package.metadata.id + $nuspecIsChocolatey = ($nuspecContent.package.files.file.src.EndsWith('chocolateyInstall.ps1').Where({ $_ }).Count -gt 0) + } + $solutionHasTools = ($null -ne (Get-Item -Path (Join-Path -Path $solutionFolderPath -ChildPath 'Tools') -ErrorAction Ignore)) + $solutionToolsHasChocoFiles = $null + if ($solutionHasTools) { + $solutionToolsHasChocoFiles = ($null -ne (Get-ChildItem -Path (Join-Path -Path (Join-Path -Path $solutionFolderPath -ChildPath 'Tools') -ChildPath "choco*.ps1"))) + } + $solution = @{ + Name = $solutionName + Project = $solutionProject + Repo = $solutionRepo + Path = $solutionPath + SolutionFile = $solutionFileObject + SolutionHasTools = $solutionHasTools + SolutionToolsHasChocoFiles = $solutionToolsHasChocoFiles + SolutionHasPackagesConfig = $solutionHasPackagesConfig + PackagesConfig = $packagesConfig + SolutionHasSemVer = $solutionHasSemVer + SemVerValueVersion = $semverValueVersion + SolutionHasNuspec = $solutionHasNuspec + SolutionNuspecIsChocolatey = $nuspecIsChocolatey + SolutionNuspecId = $solutionNuspecId + SolutionIsService = $null + SolutionIsDatabaseService = $null + IsLikelyDeprecated = $solutionName -match $obsoleteRegex -or $solutionProject -match $obsoleteRegex + Projects = @() + } + + if (Any $solutionFileObject.Projects) { + if (Any $solutionFileObject.Projects.Where({ ![string]::IsNullOrWhiteSpace($_.Path) })) { + foreach ($project in $solutionFileObject.Projects.Where({ ![string]::IsNullOrWhiteSpace($_.Path) })) { + try { + $solution.Projects += Invoke-CategorizeCSProj -Project $project + } catch { + Write-Host "$logLead : could not process project [$($project.Path)], exception was [$($_.Exception.Message)]" + } + } + } + } + + $solution.SolutionIsDatabaseService = ($solution.Projects.Where({ $null -ne $_.FluentMigrator.Version }).where({ $_ }).Count -gt 0) + $solution.SolutionIsService = ($solution.Projects.Where({ $_.ServiceInstaller.Legacy -eq $true }).where({ $_ }).Count -gt 0) + $solution.SolutionIsService = $solution.SolutionIsService -or $project.AlkamiManifest.ComponentType -eq "Service" + + return $solution + } catch { + Write-Host "$logLead : Could not process solution [$solutionPath], exception was [$($_.Exception.Message)]" + } + } + + if ($outputPathSpecified) { + $categorizeJsonFilename = "categorize.$PID.json" + $categorizePath = Join-Path -Path $OutputPath -ChildPath $categorizeJsonFilename + Set-Content -Path $categorizePath -Value (ConvertTo-Json -InputObject $solutions -Depth 50) -Force + Write-Host "File is located at $categorizePath" + } + + + $rows = @() + foreach ($solution in $solutions) { + foreach ($project in $solution.Projects) { + if ($null -eq $project) { + continue + } + if ($project.AlkamiManifest.ComponentType -eq 'Unknown') { + $project.AlkamiManifest.ComponentType = $null + } + $rows += @( + $solution.Repo + $solution.Project + $solution.Name + $solution.SemVerValueVersion + $solution.SolutionHasNuspec + $solution.SolutionNuspecIsChocolatey + $solution.SolutionNuspecId + $solution.SolutionIsService + $solution.SolutionIsDatabaseService + $project.Name + $null -ne $project.ServiceInstaller.Name + $project.Nuspec.IsChocolatey + $project.Nuspec.PackageId + $project.AlkamiManifest.ComponentType + $project.AlkamiManifest.IsLegacyInstaller + $project.AlkamiManifest.IsUnkownInstaller + $project.SemVer.Version + $project.HasTestReferences + $project.HasProgramMain + $project.AlkamiManifest.HasAlkamiManifest + $project.IsLikelyDeprecated -or $solution.IsLikelyDeprecated + $project.ChocoFiles.InstallFileSize + $project.ChocoFiles.UninstallFileSize + @($project.NewRelicAgentApi.Version)[0] + $project.FluentMigrator.Version + @($project.TopShelf.Version)[0] + $project.MicroservicesCore.Version + $project.ShouldHaveAManifestedInstaller + ($project.AlkamiManifest.HasAlkamiManifest -and $project.ChocoFiles.HasChocoFiles -and -not $project.ChocoFiles.InstallFileNew) + $project.TargetFramework + $project.TargetFrameworkValue + # $project.ServicePointManagerLines + ) -join ',' + } + } + + if (Test-IsInteractiveSession) { + $rows | Set-Clipboard + } + + if ($outputPathSpecified) { + $resultsFilename = "results.$PID.csv" + $resultsPath = Join-Path -Path $OutputPath -ChildPath $resultsFilename + Set-Content -Path $resultsPath -Value $rows + } + + $psStopWatch.Stop() + Write-Host "$logLead : Finished in $($psStopWatch.Elapsed) Seconds" + return $solutions +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeCSProj.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeCSProj.ps1 new file mode 100644 index 0000000..16acc55 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-CategorizeCSProj.ps1 @@ -0,0 +1,323 @@ +function Invoke-CategorizeCSProj { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + [ValidateNotNullOrEmpty()] + [string]$Path, + [Parameter(Mandatory = $true, ParameterSetName = 'Project')] + [ValidateNotNullOrEmpty()] + [object]$Project + ) + + $logLead = Get-LogLeadName + $projectName = $Project.Name + + $deprecatedProjectRegex = "^(zz|deprecated|archive|obsolete|xxx)" + + # This is a magic value invented by Cole for max-length of file for discrepancy on what exists + $magicChocoFilesLengthValue = 160 + + if ($PSCmdlet.ParameterSetName -eq 'Project') { + if ([string]::IsNullOrWhiteSpace($Project.Path)) { + Write-Warning "$logLead : Could not find a path property for [$($Project.Name) ($($Project.Type))]$(if ($ErrorActionPreference -ne 'Stop') { ", returning `$null"})" + return $null + } + $Path = $Project.Path + } + + if (Test-Path -Path $Path) { + $Path = Resolve-Path -Path $Path + } else { + Write-Error "$logLead : Could not resolve the path [$Path].$(if ($ErrorActionPreference -ne 'Stop') { " Returning `$null" })" + return $null + } + + Write-Verbose "$logLead : Processing child objects for csproj files" + $item = Get-Item -Path $Path + if ($item.PSIsContainer) { + $csprojList = Get-ChildItem -Path $Path -ChildPath "*.csproj" + + if (-not (Any $csprojList)) { + Write-Error "$logLead : Provided path was a Directory, and no child projects were found in the folder directly.$(if ($ErrorActionPreference -ne 'Stop') { " Returning `$null" })" + } + + $Path = $csprojList[0].FullName + Write-Warning "$logLead : Provided path was a Directory, using the first found csproj in this folder, with path [$Path]" + } + + if ($PSCmdlet.ParameterSetName -eq 'Path') { + $projectName = [System.IO.Path]::GetFileNameWithoutExtension($Path) + } + + $baseFolder = Split-Path -Path $Path -Parent + + Write-Host "$logLead : Begin processing project $($projectName)" + $csproj = @{ + Name = $projectName + Path = $Path + ChocoFiles = @{ + HasChocoFiles = $false + InstallFileSize = 0 + InstallFileNew = $false + UninstallFileSize = 0 + UninstallFileNew = $false + ChocolateyInstall = $null + ChocolateyUninstall = $null + } + Nuspec = @{ + HasNuspecFile = $false + IsChocolatey = $false + PackageId = $null + Version = $null + Raw = $null + } + AlkamiManifest = @{ + HasAlkamiManifest = $false + ComponentType = $null + AlkamiManifest = $null + IsLegacyInstaller = $false + IsUnkownInstaller = $false + } + HasTestReferences = $false + HasProgramMain = $false + Packages = $null + IsLikelyDeprecated = $Path -match $deprecatedProjectRegex + TargetFramework = 'Framework' + TargetFrameworkValue = $null + HasPostBuildEvent = $false + HasPreBuildEvent = $false + } + + $rawCsproj = [xml](Get-Content -Path $Path -Raw) + $files = Get-ChildItem -Path $baseFolder + + if (![string]::IsNullOrWhiteSpace($rawCsproj.Project.PropertyGroup.TargetFramework)) { + $csproj.TargetFrameworkValue = $rawCsproj.Project.PropertyGroup.TargetFramework + } elseif (![string]::IsNullOrWhiteSpace($rawCsproj.Project.PropertyGroup.TargetFrameworkVersion)) { + $csproj.TargetFrameworkValue = $rawCsproj.Project.PropertyGroup.TargetFrameworkVersion + } else { + $csproj.TargetFrameworkValue = 'Unknown' + } + + if ($rawCsproj.Project.PropertyGroup.TargetFramework -match 'net6' -or $rawCsproj.Project.PropertyGroup.TargetFramework -match 'netcore') { + $csproj.TargetFramework = 'core' + } elseif ($rawCsproj.Project.PropertyGroup.TargetFramework -match 'netstandard') { + $csproj.TargetFramework = 'netstandard' + } + + #region check for build events + if ($null -ne $rawCsproj.Project.PropertyGroup.PreBuildEvent) { + $csproj.HasPreBuildEvent = $true + } + if ($null -ne $rawCsproj.Project.PropertyGroup.PostBuildEvent) { + $csproj.HasPostBuildEvent = $true + } + #endregion check for build events + + $includePackagesConfig = ($rawCsproj.Project.ItemGroup.None.Include -eq 'packages.config') -or ($rawCsproj.Project.ItemGroup.Content.Include -eq 'packages.config') + $csproj.IncludesPackagesConfig = $includePackagesConfig + + Write-Verbose "$logLead : Read packages.config" + $packages = @() + $packagesConfigPath = Join-Path -Path $baseFolder -ChildPath "packages.config" + $baseFolderContainsPackagesConfig = Test-Path -Path $packagesConfigPath + if ($baseFolderContainsPackagesConfig) { + $csproj.PackagesConfigPath = $packagesConfigPath + $packagesConfig = [xml](Get-Content -Path $packagesConfigPath -Raw) + foreach ($package in $packagesConfig.packages.package) { + $packages += @{ PackageId = $package.id; Version = $package.version} + } + } + foreach ($package in $rawCsproj.Project.ItemGroup.PackageReference) { + if ([string]::IsNullOrWhiteSpace($package.Include)) { + continue + } + $packages += @{ PackageId = $package.include; Version = "$($package.version)".Trim() } + } + $csproj.Packages = $packages + + Write-Verbose "$logLead : Categorize semver" + # TODO: Recurse if not found on the base to look for tools\sem.ver a-la SDK widgets + # Record the path if not found on the project root + $semVerPath = $files.Where({$_.Name -eq "sem.ver"}).FullName + if (![string]::IsNullOrWhiteSpace($semVerPath)) { + if (Test-Path -Path $semVerPath) { + $semverValueRaw = $null + $semverValueVersion = $null + try { + $semverValueRaw = (ConvertFrom-Json (Get-Content -Path $semVerPath -Raw)) + if (($null -ne $semverValueRaw) -and ($null -ne $semverValueRaw.Version)) { + $semverValueVersion = "$($semverValueRaw.Version.Major).$($semverValueRaw.Version.Minor).$($semverValueRaw.Version.Patch)" + } + $csproj.SemVer = @{ + Version = $semverValueVersion + Raw = $semverValueRaw + } + } catch { + Write-Warning "$logLead : Could not capture the semver from [$semverPath]" + } + } + } + + #region look for specific nuget packages + Write-Verbose "$logLead : Categorize nuget packages" + $newRelicAgentApiPackage = $packages.Where({ $_.PackageId.StartsWith('NewRelic.Agent.Api', [System.StringComparison]::InvariantCultureIgnoreCase) }) + if (Any $newRelicAgentApiPackage) { + $csproj.NewRelicAgentApi = @{ + Version = @($newRelicAgentApiPackage)[0].Version + } + } + + $msCore = $packages.Where({ $_.PackageId.Equals('Alkami.MicroServices.Core', [System.StringComparison]::InvariantCultureIgnoreCase) }) + if (Any $msCore) { + $csproj.MicroservicesCore = @{ + Version = @($msCore)[0].Version + } + } + + $fluentMigratorPackage = $packages.Where({ $_.PackageId.StartsWith('FluentMigrator', [System.StringComparison]::InvariantCultureIgnoreCase) }) + if (Any $fluentMigratorPackage) { + $csproj.FluentMigrator = @{ + # only take the first one found, because there could be .Runner, .Tools, etc + Version = @($fluentMigratorPackage)[0].Version + } + } + + $topshelfPackage = $packages.Where({ $_.PackageId.StartsWith('TopShelf', [System.StringComparison]::InvariantCultureIgnoreCase) }) + if (Any $topshelfPackage) { + $csproj.TopShelf = @{ + Version = @($topshelfPackage.Version)[0].Version + } + } + + # Ends in .Tests? + foreach ($testFrameworkPrefix in @('NUnit', 'XUnit', 'MSTest')) { + $projectHasTestReferences = $packages.Where({ $_.PackageId.StartsWith($testFrameworkPrefix, [System.StringComparison]::InvariantCultureIgnoreCase) }) + $csproj.HasTestReferences = Any $projectHasTestReferences + if ($csproj.HasTestReferences) { + break + } + } + #endregion look for specific nuget packages + + #region look for manifest details + Write-Verbose "$logLead : Categorize Manifest" + $projectUsesLegacyServiceInstaller = $packages.Where({ $_.PackageId.StartsWith('Alkami.MicroServices.Installer', [System.StringComparison]::InvariantCultureIgnoreCase) }) + if (Any $projectUsesLegacyServiceInstaller) { + $csproj.AlkamiManifest.ComponentType = "Service" + $csproj.AlkamiManifest.HasAlkamiManifest = $false + $csproj.ServiceInstaller = @{ + Name = $projectUsesLegacyServiceInstaller.PackageId + Version = $projectUsesLegacyServiceInstaller.Version + IsMaster = $projectUsesLegacyServiceInstaller.PackageId -match 'Master' + Legacy = $true + } + $csproj.AlkamiManifest.IsLegacyInstaller = $true + } + + # cover the case of .json or .yaml + # Take the first one we find + $projectHasAlkamiManifestPath = @($files.Where({ $_.Name.StartsWith("AlkamiManifest") -and ($_.Name.ToLower().IndexOf("explain") -eq -1) }).FullName)[0] + $projectHasAlkamiManifest = ![string]::IsNullOrWhiteSpace($projectHasAlkamiManifestPath) + if ($projectHasAlkamiManifest) { + try { + $packageManifest = (Get-PackageManifest -Path $projectHasAlkamiManifestPath) + $csproj.AlkamiManifest.ComponentType = $packageManifest.general.componentType + $csproj.AlkamiManifest.AlkamiManifest = $packageManifest + $csproj.AlkamiManifest.HasAlkamiManifest = $true + } catch { + Write-Verbose "Something failed to process on [$projectHasAlkamiManifestPath]" + } + } else { + if (!$projectUsesLegacyServiceInstaller) { + $componentType = '' + # try to determine what type of project it should be + # TODO: Shorten the lookup by consolidating left of pipe + $providers = Get-ChildItem -Path (Join-Path -Path $baseFolder -ChildPath "*.cs") -Recurse | Select-String -Pattern "public class .+:\s+(ConnectorBase|ProviderBase)\b" | Select-Object -Property Path + $webExtensions = Get-ChildItem -Path (Join-Path -Path $baseFolder -ChildPath "*.cs") -Recurse | Select-String -Pattern "public class .+:\s+(IAlkamiWebExtension|IAlkamiModule)\b" | Select-Object -Property Path + $widgets = Get-ChildItem -Path (Join-Path -Path $baseFolder -ChildPath "*.cs") -Recurse | Select-String -Pattern "public class .+:\s+(.+WidgetDescription|.+AppRegistration)\b" | Select-Object -Property Path + if ($null -ne $providers) { + $componentType = 'Provider' + } + if ($null -ne $widgets) { + $componentType = 'Widget' + } + if ($null -ne $webExtensions) { + $componentType = 'WebExtension' + } + if ([string]::IsNullOrWhiteSpace($componentType)) { + $csProj.HasProgramMain = Get-ChildItem -Path (Join-Path -Path $baseFolder -ChildPath "*.cs") -Recurse | Select-String -Pattern "(public|private)\s?.*\s+Main\s*\(" | Select-Object -Property Path + } + $csproj.AlkamiManifest.ComponentType = $componentType + $csproj.AlkamiManifest.IsUnkownInstaller = $true + } + } + + $csProj.ServicePointManagerLines = Get-ChildItem -Path (Join-Path -Path $baseFolder -ChildPath "*.cs") -Recurse | Select-String -Pattern "ServicePointManager" | Where-Object { -not $_.Line.Trim().StartsWith("//") } | ForEach-Object { "$($_.Path):$($_.LineNumber) :: $($_.Line)" } + + Write-Verbose "$logLead : Categorize tools" + $toolsPath = Join-Path -Path $baseFolder -ChildPath "Tools" + if (Test-Path -Path $toolsPath) { + $csproj.Tools = @{ + Files = (Get-ChildItem -Path $toolsPath -File).Name + } + if ($csproj.Tools.Files -match 'choco.*\.ps1') { + $csproj.ChocoFiles.HasChocoFiles = $true + } + $chocolateyInstallPath = Join-Path -Path $toolsPath -ChildPath "chocolateyInstall.ps1" + $chocolateyUninstallPath = Join-Path -Path $toolsPath -ChildPath "chocolateyUninstall.ps1" + if (Test-Path -Path $chocolateyInstallPath) { + $csproj.ChocoFiles.InstallFileSize = (Get-Item -Path $chocolateyInstallPath).Length + if ((Get-Item -Path $chocolateyInstallPath).Length -lt $magicChocoFilesLengthValue) { + $csproj.ChocoFiles.InstallFileNew = $true + } + } + if (Test-Path -Path $chocolateyUninstallPath) { + $csproj.ChocoFiles.UninstallFileSize = (Get-Item -Path $chocolateyUninstallPath).Length + if ($csproj.ChocoFiles.UninstallFileSize -lt $magicChocoFilesLengthValue) { + $csproj.ChocoFiles.UninstallFileNew = $true + } + } + } + #endregion look for manifest details + + # above checks tools folder, this checks root folder, solution level folder check is elsewhere + $chocolateyInstallPath = Join-Path -Path $baseFolder -ChildPath "chocolateyInstall.ps1" + $chocolateyUninstallPath = Join-Path -Path $baseFolder -ChildPath "chocolateyUninstall.ps1" + if (Test-Path -Path $chocolateyInstallPath) { + $csproj.ChocoFiles.InstallFileSize = (Get-Item -Path $chocolateyInstallPath).Length + if ((Get-Item -Path $chocolateyInstallPath).Length -lt $magicChocoFilesLengthValue) { + $csproj.ChocoFiles.InstallFileNew = $true + } + } + if (Test-Path -Path $chocolateyUninstallPath) { + $csproj.ChocoFiles.UninstallFileSize = (Get-Item -Path $chocolateyUninstallPath).Length + if ($csproj.ChocoFiles.UninstallFileSize -lt $magicChocoFilesLengthValue) { + $csproj.ChocoFiles.UninstallFileNew = $true + } + } + + $csproj.ShouldHaveAManifestedInstaller = ( + ($csproj.Nuspec.IsChocolatey -and -not $csproj.AlkamiManifest.HasAlkamiManifest) -or + $true -eq $csproj.ServiceInstaller.Legacy -or + $csproj.AlkamiManifest.IsLegacyInstaller + ) + + # TODO: Tools folder nuspec? + $nuspecPath = @($files.Where({$_.Name.EndsWith(".nuspec")}).FullName)[0] + if (![string]::IsNullOrWhiteSpace($nuspecPath)) { + $nuspecContent = [xml](Get-Content $nuspecPath) + $projectNuspecId = $nuspecContent.package.metadata.id + $projectNuspecVersion = $nuspecContent.package.metadata.version + # If it doesn't have a chocolatey include, it's a problem + $nuspecIsChocolatey = ($nuspecContent.package.files.file.src.EndsWith('chocolateyInstall.ps1').where({$_}).Count -gt 0) + $csproj.Nuspec.HasNuspecFile = $true + $csproj.Nuspec.IsChocolatey = $nuspecIsChocolatey + $csproj.Nuspec.PackageId = $projectNuspecId + $csproj.Nuspec.Version = $projectNuspecVersion + $csproj.Nuspec.Raw = $nuspecContent + } + + return (New-Object -TypeName PSCustomObject -Property $csproj) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Configure.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Configure.ps1 new file mode 100644 index 0000000..d4210ee --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Configure.ps1 @@ -0,0 +1,38 @@ +function Invoke-Configure { +<# +.SYNOPSIS + Run the configure command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "configure.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No configure path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name configure -Value Invoke-Configure -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Database.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Database.ps1 new file mode 100644 index 0000000..d15d4ec --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Database.ps1 @@ -0,0 +1,38 @@ +function Invoke-Database { +<# +.SYNOPSIS + Run the database command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "db.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No database path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name db -Value Invoke-Database -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Deploy.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Deploy.ps1 new file mode 100644 index 0000000..f0e1ecb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Deploy.ps1 @@ -0,0 +1,38 @@ +function Invoke-Deploy { +<# +.SYNOPSIS + Run the deploy command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "deploy.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No deploy path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name deploy -Value Invoke-Deploy -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Hosts.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Hosts.ps1 new file mode 100644 index 0000000..633efa7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Hosts.ps1 @@ -0,0 +1,38 @@ +function Invoke-Hosts { +<# +.SYNOPSIS + Run the hosts command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "hosts.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No hosts path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name hosts -Value Invoke-Hosts -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Install.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Install.ps1 new file mode 100644 index 0000000..2ead603 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Install.ps1 @@ -0,0 +1,38 @@ +function Invoke-Install { +<# +.SYNOPSIS + Run the install command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "install.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No install path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name install -Value Invoke-Install -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-InstallerWidget.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-InstallerWidget.ps1 new file mode 100644 index 0000000..ffcbc0d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-InstallerWidget.ps1 @@ -0,0 +1,7 @@ +function Invoke-InstallerWidget { + param ( + $actingFolder + ) + + # TODO - Implement +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-JobRunner.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-JobRunner.ps1 new file mode 100644 index 0000000..3e7a3a4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-JobRunner.ps1 @@ -0,0 +1,415 @@ +function Invoke-JobRunner { + <# +.SYNOPSIS + What if invoke-parallel, but faster, with results written as jobs finish, and not waiting for the slowest batched item to complete before starting more. + +.DESCRIPTION + When using Invoke-JobRunner, if you know that all of your tasks will complete rather quickly (defined here as approximately under 2 seconds per task), + You should just invoke the work in a loop instead of trying to split your tasks up. + If your tasks will take longer than that, then this JobRunner is the correct choice. + There is no system throttling for CPU contention, so you are advised to monitor the CPU usage yourself if you are likely to break things. + +.PARAMETER JobInputs + An array of inputs for the job. Can be a primitive value or an object. Will be the first object provided to the script parameters. + +.PARAMETER ScriptBlock + A script to be executed for each JobInput + +.PARAMETER InitializationScript + Used to provide initialization ahead of the job running. + +.PARAMETER AdditionalArguments + An additional set of parameters provided after the first object in the parameter list. + These would be common arguments shared across all job entities. If you need per-job parameters, make your JobInput an Object with distinct properties. + +.PARAMETER OverallTimeoutSeconds + How long the entire Invoke-JobRunner process should run before timing out. Non-zero values will timeout in that many seconds. + This means that a 30s timeout may kill a job that has run for less than 30s if it was started after iteration 0. + +.PARAMETER JobTimeoutSeconds + How long a single job should timeout after. This value is unrelated to the OverallTimeoutSeconds value. + +.PARAMETER JobLimitSize + How many jobs should be in flight at one time. May also be referred to as Parallelization or Thread counts. + If your code uses a lot of Start-Sleep, you should increase this number to distribute the load for more workers at one time, each waiting for results/sleeps. + If your code is CPU saturating, you should leave this value as the default (8). + +.PARAMETER LoopWaitDelay + THIS VALUE IS IN SECONDS + How long to wait between checking for completed jobs. AKA Don't heat the CPU needlessly + If you know your jobs will take a long time to complete, setting this value will help the running host from going into CPU spin-state. + The best advice is to set this to half the time you think a job will take to complete, in seconds. + + Example: I know my script should take between 5 and 10 seconds to run per input. I would set this value to 3s. + Job completion checks would occur at 3s, 6s, 9s, 12s, etc. + +.PARAMETER Credential + Used for running under a given user's context + +.PARAMETER UseBatchProcessing + Legacy processing mode. Introduces batch processing when requested, but is non-preferred. See Description for details on why this is not preferred. + +.PARAMETER ReturnObjects + Should the results be returned or discarded? + +.NOTES + This was written to see if we could get faster performance from Invoke-Parallel while still getting the results back faster. + The goal is to wait for any job to complete, get all the completed jobs, then start jobs equal to the pool count until the running jobs count is the same as our pool limit. + Any one job may finish before another, so if we use `Wait-Job -All` then we have to wait for all started jobs to finish before we can read any of them. + In this method, I check to see if any jobs are finished, not all jobs, so I can work them 1 by 1. + I've also added the ability to stop jobs that go past a certain runtime to try to enforce limits for testing purposes (some jobs time out after 90 minutes, wouldn't it be nice to replicate that locally?) + The other incarnations of this function also have a memory leak, so that has been resolved as well. (keeping jobs in state instead of disposing of them once finished.) + + But wait, what about batching jobs into equal sized elements then running those in sequential process? + This method should be faster than having X worker threads each work its own queue of batched entities, except for very fast sets (the cost of instantiating new jobs is about 2s per). + Additionally, work that occurs inside the inner batched job can't be returned to the parent until the entire job finishes, which defeats the purpose of this runner. + + More details on speed versus batches follows: + + **These examples consider you are using Invoke-Parallel** + + Assume that I have some set of 16 jobs X, and 4 worker threads allocated, such that the jobs return in either 1, 2, 3, or 4 seconds. If things were truly random, I might end up with units of work taking: + Effort per job in time(s): 1 4 3 2 4 3 1 2 4 1 2 3 3 1 2 4 + So if we put those into 4 equal sized blocks we get runtimes of: + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|4 |3 |2 | = 10s + 2 - |4 |3 |1|2 | = 10s + 3 - |4 |1|2 |3 | = 10s + 4 - |3 |1|2 |4 | = 10s + Time to get all results = 10s, time to first diagnostic results = 10s + And overall the batch takes 10s to run, plus overhead, but I only get results of _any_ of the work after 10s + + What if the batch were presented in such a way as the ordering naturally tending towards + Effort per job in time(s): 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 + I have no way of knowing that this is how things will work out, but assuming that it did, we would see units of work like so: + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|1|1|1| = 4s + 2 - |2 |2 |2 |2 | = 8s + 3 - |3 |3 |3 |3 | = 9s + 4 - |4 |4 |4 |4 | = 16s + Time to get all results = 16s, time to first diagnostic results = 16s + We would not see any results until 16s. This is the worst case, clearly, and the average case is the 10s of the first example. + + **These examples consider you are using Invoke-JobRunner** + + However, this algorithm would rather approach the concept that we might use more worker threads and let new jobs enqueue as they finish, so that if we had 6 queues instead of 4, we would see: + Original example + Effort per job in time(s): 1 4 3 2 4 3 1 2 4 1 2 3 3 1 2 4 + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|1|2 |4 | + 2 - |4 |3 | + 3 - |3 |2 |2 | + 4 - |2 |4 | + 5 - |4 |1| + 6 - |3 |3 | + Time to get all results = 8s, time to first diagnostic results = 1s + We did cheat by using 2 more worker thread allocations, but now our longest iteration time was 8s, + yet we had all our diagnostic output from the other threds written within 6s, + instead of having to wait the full 10 to see any of the threads, as opposed to the original model. + The difference is we did not have to re-group or re-render worker processes to batch things, at the cost of a little more CPU usage. + + Let's re-do it with the original 4 workers, same breakdown of work as before + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|4 |1|3 | + 2 - |4 |2 |3 |4 | + 3 - |3 |1|4 |2 | + 4 - |2 |3 |2 |1| + Time to get all results = 13s, time to first diagnostic results = 1s + Now our longest timeline was 13s, instead of 10, but at 10s we had all but the last thread's complete diagnostic output and it only took a few more seconds to get the final return data to return all objects. + Being able to see all of the diagnostic output of the 15 completed jobs at 10s in is nicer for managing tasks that we rely on seeing all of the results before we can monitor progress. + + Let's turn to the second example, with 6 and then 4 worker threads, as we did last time: + + Second example + Effort per job in time(s): 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|2 |4 | + 2 - |1|2 |4 | + 3 - |1|3 |4 | + 4 - |1|3 |4 | + 5 - |2 |3 | + 6 - |2 |3 | + Time to get all results = 8s, time to first diagnostic results = 1s + + And if we look at it again with only 4 workers (original worker count, not modified 50% additional workers) + Effort per job in time(s): 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 + Worker -> Iteration timeline (|1| = 1 second, |2 | = 2s, |4 | = 4s) + 1 - |1|2 |3 |4 | + 2 - |1|2 |3 |4 | + 3 - |1|2 |3 |4 | + 4 - |1|2 |3 |4 | + Time to get all results = 10s, time to first diagnostic results = 1s + + The reason to add additional workers is because we aren't in spinlock waiting on jobs to finish, + and we can see more work getting done, + and we can start seeing the results of our work sooner, + and since we can see the impact of our work, we are able to more effectively see what is taking too long (not getting diagnostic output per input) + + The principal gained benefit of this function is the increased time to diagnostic output being returned. + The second benefit is reduced memory overhead because I'm cleaning up the job-pool as I go, which previously resulted in excess memory consumption. + + Unfortunately, for very small jobs, there is still the very unjoyous reminder that all jobs will take approximately 2 seconds to start. + This means that for sufficiently fast tasks, batching is a better option. Longer running tasks are better served by not-batching. +#> + [CmdletBinding(DefaultParameterSetName = 'ThreadPerObject')] + [OutputType([object[]])] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [Alias('Objects')] + [object[]]$JobInputs, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('Script')] + [ScriptBlock]$ScriptBlock, + + [Parameter(Mandatory = $false)] + [Alias('Arguments')] + [object[]]$AdditionalArguments, + + [Parameter()] + [ScriptBlock]$InitializationScript = $null, + + [Parameter()] + [int]$OverallTimeoutSeconds = -1, + + [Parameter()] + [int]$JobTimeoutSeconds = -1, + + [Parameter()] + [Alias('NumThreads')] + [int]$JobLimitSize = 8, + + [Parameter()] + [ValidateRange(0, 900)] + [int]$LoopWaitDelay = 0, + + [Parameter()] + [PSCredential]$Credential = $null, + + [Parameter(ParameterSetName = 'UseBatchProcessing')] + [switch]$UseBatchProcessing, + + [Parameter(ParameterSetName = 'ThreadPerObject')] + [switch]$ThreadPerObject, + + [Parameter()] + [switch]$StopProcessingJobsOnError, + + [Parameter()] + [switch]$ReturnObjects + ) + + $logLead = Get-LogLeadName + + if ($StopProcessingJobsOnError) { + Write-Warning "$logLead : This will cause your jobs to end, and may cause an unpredictable state if job scripts can not be restarted." + if (Test-IsInteractiveSession) { + Write-Warning "$logLead : You have 5 seconds to hit ctrl-c and re-evaluate your life choices. Inconsistent states on job-termination can be disastrous." + Start-Sleep -Seconds 5 + } + } + + # If the user input did not specify a value, we should supply a default + if ($LoopWaitDelay -eq 0) { + # The goal here is to not stay in a spin-lock on the OS waiting for jobs to finish, because jobs will usually take longer than 150ms to complete. + # Basically, when PS hits a Start-Sleep (as we use below), the OS knows to background it. + # The value here is actually kind of counter intuitive but it's the right-ish way to go. + # If you want more details, you should lookup System.Activites.Bookmark and System.Activites.Delay + # The reason for twice is to prevent CPU spin, but on short-tasks (under 20ms) they will be completed long before we check for results. + # We likely want to revisit and set this to be a higher multiplier-value in the future + + # Allow the system two thread worker attempts before we re-entry on the job to look for updated thread completion + $systemTimeSlice = Get-WindowsThreadSliceTime -Milliseconds + $sleepTimeSlice = $systemTimeSlice * 2 + } else { + # Convert to milliseconds (they can enter up to 900 on the loop delay which is 15 minutes) + $sleepTimeSlice = $LoopWaitDelay * 1000 + } + + if ($null -eq $InitializationScript) { + # I'm greedy, I want us to use more CPU than the underlying system host + $InitializationScript = { (Get-Process -Id $PID).PriorityClass = 'AboveNormal' } + } + + $psStopWatch = [System.Diagnostics.Stopwatch]::StartNew() + $hitProcessTimeoutState = $false + + $progressActivityLabel = "$logLead$PID" + Write-Progress -Activity $progressActivityLabel -Status 'Processing inputs' -PercentComplete 0 + + $transformedJobInputs = New-Object -TypeName "System.Collections.ArrayList" + foreach ($jobInput in $JobInputs) { + $transformedJobInputs.Add($jobInput) | Out-Null + } + + # Could store them on the array like this to have more data + $jobs = New-Object -TypeName "System.Collections.ArrayList" + $results = @() + + Write-Host "$logLead : Starting on $($transformedJobInputs.Count) elements" + + [ScriptBlock]$batchScript = $null + if ($UseBatchProcessing) { + # Define script that runs per thread. + $batchScript = { + param( + [object]$script, + [object[]]$ArgumentList + ) + + # Deserialize script block, turn it into a script block again. + $script = [scriptblock]::Create($script) + + # Invoke user-provided script block on each object. + # SRE-13225 - The ErrorAction on Invoke-Command does NOT affect what happens INSIDE + # the ScriptBlock. Because we're batching things to be parallelized on "shared threads" + # this ErrorAction allows us to have a failure in the middle of a batch without + # halting the entire batch. + foreach ($object in $objects) { + Invoke-Command -ErrorAction Continue -ScriptBlock $script -ArgumentList $ArgumentList -NoNewScope + } + } + + # Redefine JobInputs array into batches instead of single entities + $batchSize = [Math]::Ceiling($JobInputs.Count / $JobLimitSize) + $batchIterator = 0 + $batchSlice = $null + $transformedJobInputs = New-Object -TypeName "System.Collections.ArrayList" + while (($batchSlice = $JobInputs[$batchIterator..$($batchIterator + $batchSize - 1)]) -and (!(Test-IsCollectionNullOrEmpty $batchSlice)) -and ($batchIterator += $batchSize)) { + Write-Debug "$logLead : Created a batch of $($batchSlice.Count) elements with values: [$($batchSlice -join ',')]" + $transformedJobInputs.Add($batchSlice) | Out-Null + } + } + + $totalJobCount = $transformedJobInputs.Count + + Write-Progress -Activity $progressActivityLabel -Status 'Running threads' -PercentComplete 0 + + while ($jobs.Count -gt 0 -or $transformedJobInputs.Count -gt 0) { + $removeJobs = @() + # now receive completed jobs + foreach ($job in $jobs) { + $removeJob = $false + $jobId = $job.Id + $jobStatus = Get-Job -Id $jobId + if ($OverallTimeoutSeconds -gt 0) { + if ($psStopWatch.Elapsed.TotalSeconds -gt $OverallTimeoutSeconds) { + <# Inline double-if because PS doesn't support eager evaluation of boolean states and I want to go faster when 0 #> + Stop-Job -Id $jobId | Out-Null + Write-Host "$logLead : Stopped job with id $jobId because it has exceeded the allowed overall timeout value of $OverallTimeoutSeconds seconds. Input object was [$($job.JobInput)]" + $removeJob = $true + $hitProcessTimeoutState = $true + } + } + if ($JobTimeoutSeconds -gt 0) { + if (([System.DateTime]::Now - $job.StartTime).TotalSeconds -gt $JobTimeoutSeconds) { + <# Inline double-if because PS doesn't support eager evaluation of boolean states and I want to go faster when 0 #> + Stop-Job -Id $jobId | Out-Null + Write-Host "$logLead : Stopped job with id $jobId because it has exceeded the allowed job timeout value of $JobTimeoutSeconds seconds. Input object was [$($job.JobInput)]" + $removeJob = $true + } + } + $jobStatus = Get-Job -Id $jobId + if ($jobStatus.State -in @('Stopped', 'Failed', 'Completed', 'Disconnected')) { + if ($ReturnObjects) { + $results += Receive-Job -Id $jobId -ErrorAction Continue + } else { + Receive-Job -Id $jobId -ErrorAction Continue + } + if ($jobStatus.State -in @('Failed', 'Disconnected')) { + Write-Warning "$logLead : Job with ID [$jobId] has failed with JobStateInfo.Reason [$($jobStatus.ChildJobs[0].JobStateInfo.Reason)]" + Write-Warning "$logLead : Job with ID [$jobId] had input [$($job.JobInput)]" + # $throwAtEnd += $job + + # Legacy behavior to stop processing jobs when something errors + # This could leave things in an incomplete state on prior threads because we force stopping the jobs + if ($StopProcessingJobsOnError) { + $hitProcessTimeoutState = $true + } + } + Write-Verbose "$logLead : Received job with id $jobId" + Remove-Job -Id $jobId | Out-Null # ensure the job is disposed of + $removeJob = $true + } + if ($removeJob) { + $removeJobs += $job + } + } + foreach ($job in $removeJobs) { + $jobs.Remove($job) | Out-Null + Write-Verbose "$logLead : Removed job with id $($job.Id)" + } + Write-Progress -Activity $progressActivityLabel -PercentComplete (100 * ($totalJobCount - $transformedJobInputs.Count) / $totalJobCount) + if ($hitProcessTimeoutState) { + if ($transformedJobInputs.Count -gt 0) { + Write-Error "$logLead : We hit the job timeout stage, no further jobs will be processed. There are still $($transformedJobInputs.Count) remaining jobs that did not get started." + Write-Host "$logLead : The following job inputs did not start processing: [$((ConvertTo-Json $transformedJobInputs -Depth 5))]" + } + if ($jobs.Count -gt 0) { + Write-Warning "$logLead : Additional jobs still processing will now be stopped." + foreach ($job in $jobs) { + $jobId = $job.Id + Stop-Job -Id $jobId | Out-Null + Write-Host "$logLead : Stopped job with id $jobId because it has exceeded the allowed overall timeout value of $OverallTimeoutSeconds seconds. Input object was [$($job.JobInput)]" + if ($ReturnObjects) { + $results += Receive-Job -Id $jobId -ErrorAction Continue + } else { + Receive-Job -Id $jobId -ErrorAction Continue + } + Write-Verbose "$logLead : Received job with id $jobId" + Remove-Job -Id $jobId | Out-Null # ensure the job is disposed of + } + } + break + } + while ($jobs.Count -le $jobLimitSize -and $transformedJobInputs.Count -gt 0) { + # add new jobs + $originalJobInput = $transformedJobInputs[0] + # Grab next batch to start instead of one per thread + $ArgumentList = New-Object -TypeName "System.Collections.ArrayList" + $ArgumentList.Add($originalJobInput) | Out-Null + foreach ($additionalArgument in $AdditionalArguments) { + $ArgumentList.Add($additionalArgument) | Out-Null + } + $splat = @{ + ScriptBlock = $ScriptBlock + ArgumentList = $ArgumentList + InitializationScript = $InitializationScript + } + if ($UseBatchProcessing) { + $splat.ScriptBlock = $batchScript + $splat.ArgumentList = $ScriptBlock, $ArgumentList + } + if ($null -ne $Credential) { + $splat.Credential = $Credential + } + $job = Start-Job @splat + $jobState = @{ Id = $job.Id; JobInput = (ConvertTo-Json $originalJobInput -Compress -Depth 5); StartTime = [System.DateTime]::Now; } + $jobs.Add($jobState) | Out-Null + # Wait-Job -Id $job.Id -Timeout 0 | Out-Null # stick it on the background + Write-Verbose "$logLead : Started job with id $($job.Id)" + $transformedJobInputs.RemoveAt(0) | Out-Null # We know we are done when this array is empty. It was a clone of the original input. + } + $currentJobsIds = @() + foreach ($job in $jobs) { + $currentJobsIds += $job.Id + } + if ($currentJobsIds.Count -gt 0) { + Write-Debug "$logLead : Waiting on jobs $currentJobsIds" + Wait-Job -Any -Id $currentJobsIds -Timeout 0 | Out-Null + } + + # Introduce a little jitter so that jobs have a chance to finish before we check for results. + Start-Sleep -Milliseconds $sleepTimeSlice + } # end outer while + + Write-Host "$logLead : Finished in $($psStopWatch.Elapsed) seconds" + $psStopWatch.Stop() + Write-Progress -Activity $progressActivityLabel -Completed + + if ($ReturnObjects) { + return $results + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Notes.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Notes.ps1 new file mode 100644 index 0000000..6685b53 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Notes.ps1 @@ -0,0 +1,128 @@ +Function Invoke-Notes { +<# +.SYNOPSIS + Play some musical notes using the console beep + +.PARAMETER Notes + The notes values to be played. Consists of three parts: Pitch, Octave (optional), NoteType (optional) + Defaults to the octave "Treble C". Supports octaves from Subcontra to Five-lined (triple-high-C) + Defaults to quarternotes when none are provided. + +.PARAMETER Tempo + Speed-change for duration of notes + +.PARAMETER Output + Debug for showing what is being calculated + +.NOTES + Found this on Reddit, copied it down for George. Enjoy George. + +.LINK + https://www.reddit.com/r/PowerShell/comments/q8l24k/some_powershell_beep/ + +.LINK + https://www.reddit.com/r/PowerShell/comments/8wj4cu/i_wrote_a_powershell_script_that_uses_the_console/ + +.LINK + https://en.wikipedia.org/wiki/C_(musical_note) + +.EXAMPLE +# Play a G eigth note in the 6th octave +# Play a F# eigth note in the 6th octave +# Play a G eigth note in the 6th octave +Invoke-Notes -Notes "G6E,F#6E,G6E" -Output +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Notes, + + [Parameter(Mandatory = $false)] + [int]$Tempo, + + [Parameter(Mandatory = $false)] + [switch]$Output = $false + ) + + $tempoModifier = 1 + $defaultNoteType = 'Q' + $defaultOctave = 5 + + if ($Tempo -gt 0) { + $tempoModifier = 100.0/$Tempo + } elseif ($Tempo -lt 0) { + Write-Warning "Tempo value supplied [$Tempo] is invalid (less than zero). It is ignored." + } + + # W = Whole, H = Half, Q = Quarter, E = Eighth, S = Sixteenth + $NoteTypes = [PSCustomObject]@{ + 'W' = 1600 + 'W.' = 2000 + 'H' = 800 + 'H.' = 1000 + 'Q' = 400 + 'Q.' = 600 + 'E' = 200 + 'E.' = 300 + 'S' = 100 + 'S.' = 150 + } + + # Define the Subcontra octave frequencies + # We will calculate more octaves based from these + $NoteIndex = [PSCustomObject]@{ + 'C' = @(16.3516) + 'C#' = @(17.32391) + 'D' = @(18.35405) + 'Eb' = @(19.44544) + 'E' = @(20.60172) + 'F' = @(21.82676) + 'F#' = @(23.12465) + 'G' = @(24.49971) + 'G#' = @(25.95654) + 'A' = @(27.50000) + 'Bb' = @(29.13524) + 'B' = @(30.86771) + <#Rest#>'R' = 0 + } + + foreach($note in $NoteIndex.PSObject.Properties.Name) { + $seed = $NoteIndex.$note[0] + $newValues = @([Math]::Round($seed,2)) + # calculate 8 more octaves above the seed data + foreach($mult in 0..7) { + # this is the operation for 2^x -> (2 -shl $mult) + $newValues += [Math]::Round( ($seed * (2 -shl $mult)) ,2) + } + $noteIndex.$note = $newValues + } + + $splitNotes = ($Notes -split ',') + + foreach ($Note in $splitNotes) { + $Note -match '(?[A-G][#|b]?|[R])(?[0-8])?(?[Ww|Hh|Qq|Ee|Ss][\.]?)?' | Out-Null + $Pitch = $matches['Pitch'] + $noteType = $matches['NoteType'] + $octave = $matches['Octave'] + + if ($null -eq $noteType) { + $noteType = $defaultNoteType + } + if ($null -eq $octave) { + $octave = $defaultOctave + } + + [int]$Duration = $tempoModifier * ($NoteTypes.$noteType) + [int]$Frequency = $NoteIndex.$Pitch[$octave] + + if ($Output) { + Write-Host "$Pitch $octave $noteType - $Duration - $Frequency" + } + + if ($Pitch -eq 'R') { + Start-Sleep -Milliseconds $Duration + } else { + [Console]::beep($Frequency,$Duration) + } + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Package.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Package.ps1 new file mode 100644 index 0000000..ff1346b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Package.ps1 @@ -0,0 +1,38 @@ +function Invoke-Package { +<# +.SYNOPSIS + Run the package command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "package.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No package path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name package -Value Invoke-Package -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-QueryByConnectionString.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-QueryByConnectionString.ps1 new file mode 100644 index 0000000..40dea83 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-QueryByConnectionString.ps1 @@ -0,0 +1,85 @@ +function Invoke-QueryByConnectionString { + + param ( + [Parameter(Mandatory = $true)] + [string]$ConnectionString, + + [Parameter(Mandatory = $true)] + [string]$QueryString + ) + + $logLead = Get-LogLeadName + $conn = New-Object System.Data.SqlClient.SqlConnection + + try { + + $conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($ConnectionString) + + } catch [System.Management.Automation.MethodException] { + + Write-Warning "$logLead : Provided connection string [$ConnectionString] is invalid. Execution cannot continue." + return $null + } + + $conn.ConnectionString = $conStrBuilder.ToString() + Write-Verbose ("$logLead : Connecting to database with connection string {0}" -f $conStrBuilder.ToString()) + + if (-not (Confirm-DatabaseAccess -ConnectionString $conStrBuilder.ToString())) { + Write-Error "$logLead : You can not connect to the database server, do you have access and is it online?" + return + } + + $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { + param($sender, $event) + Write-Host $event.Message + }; + + try { + + $conn.add_InfoMessage($handler); + $conn.FireInfoMessageEventOnUserErrors = $true + + $conn.Open() + $query = New-Object System.Data.SqlClient.SqlCommand($QueryString, $conn) + $reader = $query.ExecuteReader() + + $allResults = New-Object -TypeName "System.Collections.ArrayList" + do { + $currentResults = @() + $columns = @() + for ($i = 0; $i -lt $reader.FieldCount; $i++) { + $columns += $reader.GetName($i) + } + while ($reader.Read()) { + $rowResult = @{} + for ($i = 0; $i -lt $reader.FieldCount; $i++) { + if ($reader.IsDBNull($i)) { + $rowResult[$columns[$i]] = $null + } else { + $rowResult[$columns[$i]] = $reader[$i] + } + } + $currentResults += $rowResult + } + $allResults.Add($currentResults) | Out-Null + } while ($reader.NextResult()) + + return $allResults + + } catch { + + Write-Warning "$logLead : An exception occurred while trying to execute the specified query against the database" + Write-Warning "$logLead : $($_.ToString())" + Write-Warning "$logLead : $($_.ScriptStackTrace)" + return $null + + } finally { + + if (($null -ne $conn) -and ($conn.State -ne [System.Data.ConnectionState]::Closed)) { + + $conn.Close() + } + + $conn = $null + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-ScriptedActions.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-ScriptedActions.ps1 new file mode 100644 index 0000000..393bd8b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-ScriptedActions.ps1 @@ -0,0 +1,60 @@ +function Invoke-ScriptedActions { + param ( + [object[]]$Actions, + [string]$PromptText = ((prompt | Out-String) -replace "`n",'' -replace "`r",''), + [int]$PromptWatch = 800, + [int]$TypingDelay = 150, + [int]$TypingDelayJitterMin = 50, + [int]$TypingDelayJitterMax = 100, + [int]$PasteDelay = 1800, + [int]$PasteDelayJitterMin = 600, + [int]$PasteDelayJitterMax = 3000, + [int]$PasteWatch = 250, + [switch]$TurboMode + ) + + Write-Host -NoNewLine $PromptText + Start-Sleep -Milliseconds $PromptWatch + + foreach ($action in $Actions) { + $actionType = $action.Keys[0] + $actionValue = $action.Values[0] + switch ($action.Keys[0]) { + 'Paste' { + Start-Sleep -Milliseconds $PasteDelay + Write-Host -NoNewLine $actionValue + Start-Sleep -Milliseconds $PasteWatch + } + 'Type' { + $chars = $actionValue -split '' + for ($i = 0; $i -lt $chars.Length; $i++) { + $char = $chars[$i] + Write-Host -NoNewLine $char + if ($i -lt ($chars.Length - 1)) { + Start-Sleep -Milliseconds $TypingDelay + } + } + } + 'Tab' { + Write-Host -NoNewLine $actionValue + } + 'Delay' { + Start-Sleep -Milliseconds "$actionValue" + } + 'TabChange' { + Write-Host -NoNewLine $actionValue + Start-Sleep -Milliseconds ($TypingDelay * 2) + $length = $actionValue.Length + $backspace = "`b" + $space = " " + $backspaces = $backspace * $length + $spaces = $space * $length + $overwriteWord = "$backspaces$spaces$backspaces" + + Write-Host -NoNewLine $overwriteWord + } + default {} + } + } + Write-Host "" +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Shutdown.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Shutdown.ps1 new file mode 100644 index 0000000..a80ff92 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Shutdown.ps1 @@ -0,0 +1,67 @@ +function Invoke-Shutdown { +<# +.SYNOPSIS + Wrap the default Windows shutdown command with more helper information + You should just use Stop-Computer or Restart-Computer + +.PARAMETER Now + Shutdown the current computer now + +.PARAMETER Reboot + Reboot on shutdown + +.PARAMETER ComputerName + One or more computers to shut down. + +.PARAMETER Minutes + How long to wait before shutting down. Defaults to 1 minute. + +.PARAMETER Reason + Provide a reason why we are shutting this computer down +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(ParameterSetName = 'Now')] + [switch]$Now, + [Parameter(ParameterSetName = 'Computer', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$ComputerName, + [Parameter(ParameterSetName = 'Computer', Mandatory = $false)] + [int]$Minutes = 1, + [Parameter()] + [switch]$Reboot, + [Parameter()] + [string]$Reason + ) + + $arguments = @("/s") # shutdown command + if ($Reboot) { + $arguments = @("/r") # reboot, not shutdown + } + if (-not [string]::IsNullOrWhiteSpace($Reason)) { + $arguments += "/c `"$Reason`"" + } + $arguments += "/f" # force it to kill apps + + if ($PSCmdlet.ParameterSetName -eq 'Now') { + Write-Host "Shutting system down now" + $arguments += "/t" + $arguments += "0" # now + Invoke-CallOperatorWithPathAndParameters -Path "C:\Windows\system32\shutdown.exe" -Arguments $arguments + } else { + # The timeout value is expressed in seconds, not minutes + $arguments += "/t" + $arguments += "$($Minutes * 60)" + foreach ($computer in $ComputerName) { + if (-not [string]::IsNullOrWhiteSpace($computer)) { + Write-Host "Shutting down remote server $computer" + $computerArguments = @($arguments) + $computerArguments += "/m" + $computerArguments += "\\$computer" + Write-Host "C:\Windows\system32\shutdown.exe $($computerArguments -join ' ')" + Invoke-CallOperatorWithPathAndParameters -Path "C:\Windows\system32\shutdown.exe" -Arguments $computerArguments + } + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Invoke-Test.ps1 b/Modules/Cole.PowerShell.Developer/Public/Invoke-Test.ps1 new file mode 100644 index 0000000..3ba47fd --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Invoke-Test.ps1 @@ -0,0 +1,38 @@ +function Invoke-Test { +<# +.SYNOPSIS + Run the test command in this or the appropriate parent folder from this folder +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments = $true)] + $CommandLineArguments + ) + + $logLead = (Get-LogLeadName) + + $currentFolder = (Get-Location).Path + $taskPath = $null + + do { + $taskPath = $null + $potentialPath = (Join-Path -Path $currentFolder -ChildPath "test.ps1") + if (Test-Path $potentialPath) { + $taskPath = $potentialPath + break + } + $currentFolder = (Get-Item $currentFolder).Parent.Name + # If the current folder is the root, it has no parent, so we can't recurse again + if ($null -eq $currentFolder) { + break + } + } while ([string]::IsNullOrWhiteSpace($taskPath)) + + if ([string]::IsNullOrWhiteSpace($taskPath)) { + Write-Warning "$logLead : No test path could be found in current or parent paths." + return $null + } + + Invoke-Expression "$taskPath $($CommandLineArguments -join ' ')" +} +New-Alias -Name test -Value Invoke-Test -Force diff --git a/Modules/Cole.PowerShell.Developer/Public/Join-UrlComponents.ps1 b/Modules/Cole.PowerShell.Developer/Public/Join-UrlComponents.ps1 new file mode 100644 index 0000000..714956f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Join-UrlComponents.ps1 @@ -0,0 +1,276 @@ +function Join-UrlComponents { +<# +.SYNOPSIS + Used to join together the parts of a URL based on inputs + +.PARAMETER UrlComponents + An object of url components. Typically comprised of one or more of the other parameters on this method + +.PARAMETER BaseUrl + A [System.Uri] (or string representation thereof) to start the Url from + +.PARAMETER Hostname + A hostname (or IP address) to connect to + +.PARAMETER Scheme + The connection scheme. Ex: http, https. Defaults to http. + +.PARAMETER Port + The port to be used to connect to. Can be null. + +.PARAMETER Path + The path to connect to. Can supply one or more values + +.PARAMETER Query + The query string to append to the Url. Can supply one or more values + +.PARAMETER Fragment + The fragment to append to the Url. Can supply one or more values + +.PARAMETER Username + The username for the connection. This is typically frowned upon. Prefer a secure header. + +.PARAMETER SecureString + The password for the connection. This is typically frowned upon. Prefer a secure header. + +.PARAMETER Credential + The username and password for the connection. This is typically frowned upon. Prefer a secure header. +#> + [CmdletBinding(DefaultParameterSetName = 'PartsWithUsername')] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'UrlComponents', ValueFromPipeline = $true)] + [object]$UrlComponents, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithCredential')] + [string]$BaseUrl, + [Parameter(Mandatory = $true, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $true, ParameterSetName = 'PartsWithCredential')] + [Alias("Host")] + [string]$Hostname, + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [string]$Scheme, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [AllowNull()] + [Nullable[uint16]]$Port, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [string[]]$Path, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [object]$Query, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [string]$Fragment, + + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [string]$Username, + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithUsername')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithUsername')] + [Alias("Password")] + [SecureString]$SecureString, + + [Parameter(Mandatory = $false, ParameterSetName = 'BaseUrlWithCredential')] + [Parameter(Mandatory = $false, ParameterSetName = 'PartsWithCredential')] + [PSCredential]$Credential + ) + + $logLead = (Get-LogLeadName) + + $password = $null + $schemeDelimiter = "://" + $isDefaultPort = $false + $defaultPorts = @{ + ftp = 21; + ssh = 22; + telnet = 23; + mailto = 25; + http = 80; + ldap = 389; + https = 443; + "net.tcp" = 808; + } + + if ($PSCmdlet.ParameterSetName -eq 'UrlComponents') { + if ($null -eq $UrlComponents) { + throw "$logLead : `$null values can not be supplied for the UrlComponent object" + } + + # Set the other values to the defaults provided by this object if present + $BaseUrl = $UrlComponents.BaseUrl + $Hostname = $UrlComponents.Hostname + $Scheme = $UrlComponents.Scheme + $Port = $UrlComponents.Port + $pathCoalesce = Coalesce $UrlComponents.Segments $UrlComponents.Path + $Path = @($pathCoalesce) + $Query = @($UrlComponents.Query) + $Fragment = $UrlComponents.Fragment + $Username = $UrlComponents.Username + $password = $UrlComponents.Password + $Credential = $UrlComponents.Credential + } + + if ($null -ne $SecureString) { + $password = (ConvertFrom-SecureString $SecureString) + } + if ($null -ne $Credential) { + if ([string]::IsNullOrWhiteSpace($Username)) { + $Username = $Credential.Username + } + if ([string]::IsNullOrWhiteSpace($password)) { + $password = (Get-PasswordFromCredential -Credential $Credential) + } + } + + if (![string]::IsNullOrWhiteSpace($BaseUrl)) { + # got a baseurl, see if there are assignable values that weren't also supplied + $uri = [Uri]::new($BaseUrl) + $Hostname = Coalesce $Hostname $uri.Host + $Scheme = Coalesce $Scheme $uri.Scheme 'http' + $Port = Coalesce $Port $uri.Port + $Path = Coalesce $Path $uri.Segments + $Query = Coalesce $Query $uri.Query + $Fragment = Coalesce $Fragment $uri.Fragment + } + + if ([string]::IsNullOrWhiteSpace($Hostname)) { + throw "$logLead : Unable to determine any hostname property for this url from provided parameters. Can not continue." + } + + if ([string]::IsNullOrWhiteSpace($Scheme)) { + $Scheme = 'http' + } + + if (($null -eq $Port) -or ($Port -eq 0)) { + if (![string]::IsNullOrWhiteSpace($Scheme)) { + $Port = $defaultPorts[$Scheme] + } + } + + $isDefaultPort = $Port -eq $defaultPorts[$Scheme] + + $formattedUsernameAndPassword = "" + if (![string]::IsNullOrWhiteSpace($Username)) { + if (![string]::IsNullOrWhiteSpace($password)) { + $formattedPassword = ":$password" + } + $formattedUsernameAndPassword = "$Username$formattedPassword" + if (![string]::IsNullOrWhiteSpace($formattedUsernameAndPassword)) { + $formattedUsernameAndPassword = "$formattedUsernameAndPassword@" + } + } + + $formattedPort = "" + if (!$isDefaultPort) { + $formattedPort = ":$Port" + } + + $pathSegments = @() + $addTrailingSlash = $false + foreach ($pathSegment in $Path) { + $pathSegment = $pathSegment.TrimStart("/") + $addTrailingSlash = $pathSegment.EndsWith("/") + $pathSegment = $pathSegment.TrimEnd("/") + # Can't use empty segments. + # While a webserver may be able to interpret http://example.com/////path, we won't do that to the poor webserver. It'll just get http://example.com/path instead + if (![string]::IsNullOrWhiteSpace($pathSegment)) { + $pathSegments += $pathSegment + } + } + $formattedPath = $pathSegments -join '/' + # If the last path segment we saw ended with a slash, put that back on the end of the url. + # Example: REST apis may end with a slash, so Path = @("search/") => "/search/" + # Example: Path = @("search/","page.html") => "/search/page.html" + if (![string]::IsNullOrWhiteSpace($formattedPath) -and $addTrailingSlash) { + $formattedPath = "$formattedPath/" + } + + $querySegments = @() + $querySegmentKeys = @("Name","Key","Id") + if ($Query -is [System.Collections.IEnumerable] -and $Query -isnot [string]) { + $keys = $Query.Keys + if (!(Any $keys)) { + $keys = @($Query.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}).Name) + } + if (!(Any $keys)) { + $keys = @($Query.PSObject.Properties.Where({$_.Name -eq 'Keys'}).Value) + } + if (Any $keys) { + # must be a hash object, take the names and values + foreach ($key in $keys) { + $value = $Query.$key + if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { + $value = $value -join ',' + } + $querySegments += "$key=$value" + } + } else { + foreach ($querySegment in $Query) { + if ($querySegment -is [string]) { + $querySegment = $querySegment.TrimStart("?") + $splitSegments = $querySegment -split '&' + foreach ($splitSegment in $splitSegments) { + if (![string]::IsNullOrWhiteSpace($splitSegment)) { + $querySegments += $splitSegment + } + } + } else { + $key = "" + foreach ($queryKey in $querySegmentKeys) { + if (![string]::IsNullOrWhiteSpace($querySegment.$queryKey)) { + $key = $querySegment.Key + } + } + if ([string]::IsNullOrWhiteSpace($key)) { + continue + } + $value = $querySegment.Value + if ($null -ne $value) { + if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { + $value = $value -join ',' + } + $querySegments += "$key=$value" + } else { + Write-Warning "$logLead : Found querySegment with matching key but no .Value element" + continue + } + } + } + } + } elseif ($Query -is [string]) { + $querySegment = $Query.TrimStart("?") + $splitSegments = $querySegment -split '&' + foreach ($splitSegment in $splitSegments) { + if (![string]::IsNullOrWhiteSpace($splitSegment)) { + $querySegments += $splitSegment + } + } + } else { + Write-Verbose "$logLead : No `$Query value to process, or can't process it" + } + + $formattedQuery = "" + if (Any $querySegments) { + $formattedQuery = $querySegments -join '&' + if (![string]::IsNullOrWhiteSpace($formattedQuery)) { + $formattedQuery = "?$formattedQuery" + } + } + + $formattedFragment = "" + if (![string]::IsNullOrWhiteSpace($Fragment)) { + $formattedFragment = $formattedFragment.TrimStart('#') + $formattedFragment = "#$formattedFragment" + } + + $formattedUrl = "$scheme$schemeDelimiter$formattedUsernameAndPassword$Hostname$formattedPort/$formattedPath$formattedQuery$formattedFragment" + + return $formattedUrl +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Merge-Objects.ps1 b/Modules/Cole.PowerShell.Developer/Public/Merge-Objects.ps1 new file mode 100644 index 0000000..8ce0ee5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Merge-Objects.ps1 @@ -0,0 +1,90 @@ +function Merge-Objects { +<# +.SYNOPSIS + Used to merge two or more objects into a single output. + Two objects with the same name will replace the value of the former with the latter. + If two properties can be merged, they will be, as far down the stack as we can go. + +.EXAMPLE + $FoundUser1 = [pscustomobject] @{ + 'Duplicate Group1' = 'test1' + 'User2' = 'test2' + } + + $FoundUser2 = [pscustomobject] @{ + 'Duplicate Group2' = 'test3' + 'User1' = 'test4' + } + + Merge-Objects -Objects $FoundUser1, $FoundUser2 + +.EXAMPLE + Mixin @{ cole = "name" },@{ test = @{ result = "passed" } },@{ cole = "replaced" },@{ test = @{ result = "double passed"; reason = "success" } } +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Object[]]$Objects, + [switch]$DontClobber, + [switch]$DontDeepMerge + ) + + $logLead = (Get-LogLeadName) + + # ensure it's an array of objects so we can iterate + $Objects = @($Objects) + + $returnObject = [Ordered]@{} + + $restrictedPropertyNames = @( + 'IsReadOnly' + 'IsFixedSize' + 'IsSynchronized' + 'Keys' + 'Values' + 'SyncRoot' + 'Count' + ) + + foreach($object in $Objects) { + $properties = @($object.Keys) + if (!(Any $properties)) { + $properties = @($object.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}).Name) + } + if (!(Any $properties)) { + Write-Verbose "One of the objects presented does not have a parseable keys object property" + # Write-Verbose $object + continue + } + foreach ($property in $properties) { + if ($restrictedPropertyNames -contains $property) { + continue + } + if ($returnObject.Keys -contains $property) { + Write-Verbose "`$returnObject.$property.GetType() = [$(($returnObject.$property).GetType())]" + Write-Verbose "`$object.$property.GetType() = [$(($object.$property).GetType())]" + if (!$DontDeepMerge -and (($returnObject.$property).GetType().Name -eq 'Hashtable') -and (($object.$property).GetType().Name -eq 'Hashtable')) { + Write-Host "Merging two hashtables" + $temp = Merge-Objects @($returnObject.$property,$object.$property) + $returnObject.$property = $temp + } else { + if ($DontClobber) { + $temp = $returnObject.$property + $returnObject.$property = @($temp) + $returnObject.$property += $object.$property + } else { + Write-Verbose "$logLead : New object clobbered the property [$property] from a previous object." + $returnObject.$property = $object.$property + } + } + } else { + $returnObject += @{$property = $object.$property} + } + } + } + + return [PSCustomObject]$returnObject +} + +Set-Alias -Name Combine -Value Merge-Objects +Set-Alias -Name Mixin -Value Merge-Objects \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-AWSConfigFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-AWSConfigFile.ps1 new file mode 100644 index 0000000..ccf13ba --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-AWSConfigFile.ps1 @@ -0,0 +1,89 @@ +function New-AWSConfigFile { +<# +.SYNOPSIS + Used to setup a new AWS Config File +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + $FilePath, + [Parameter(Mandatory = $true)] + $virtualMFADeviceSerialNumber, + [Parameter(Mandatory = $true)] + $RoleName + ) + + $logLead = (Get-LogLeadName) + + if (Test-Path $FilePath) { + throw "$logLead : Can not replace existing AWS Config file at [$FilePath]. Please remove the file and try again." + } + + $fileContents = @" +#Place this file in your .aws folder, overwriting the existing file. +#Set up your credentials file to use your master payer keys as the default profile +#Fill in the brackets below with the required information. + +[default] +#transit account with access to only manage MFA and assumerole to assigned role(s) which will now prompt for MFA +region = us-east-1 +output = json + +[profile Prod] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::790953160341:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Transit] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::844547943473:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Transitnp] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::727029306845:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Qa] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::668894625708:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Sandbox] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::490361062173:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Security] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::228368111183:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Dev] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::327695573722:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Corp] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::994898437262:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber + +[profile Mp] +region = us-east-1 +source_profile = default +role_arn = arn:aws:iam::185809956479:role/$RoleName +mfa_serial = $virtualMFADeviceSerialNumber +"@ + + Set-Content -Path $FilePath -Value $fileContents -Force +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-AWSCredentialsFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-AWSCredentialsFile.ps1 new file mode 100644 index 0000000..bac9638 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-AWSCredentialsFile.ps1 @@ -0,0 +1,40 @@ +function New-AWSCredentialsFile { +<# +.SYNOPSIS + Used to setup a new AWS Config File +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + $FilePath + ) + + $logLead = (Get-LogLeadName) + + if (Test-Path $FilePath) { + throw "$logLead : Can not replace existing AWS Credentials file at [$FilePath]. Please remove the file and try again." + } + + $fileContents = @" +[default] +aws_access_key_id = +aws_secret_access_key = +output=json +region=us-east-1 +[temp-dev] +aws_access_key_id= +aws_secret_access_key= +aws_session_token= +[temp-prod] +aws_access_key_id= +aws_secret_access_key= +aws_session_token= +[temp-qa] +aws_access_key_id= +aws_secret_access_key= +aws_session_token= +"@ + + Set-Content -Path $FilePath -Value $fileContents -Force +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-Directory.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-Directory.ps1 new file mode 100644 index 0000000..842e6c7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-Directory.ps1 @@ -0,0 +1,25 @@ +function New-Directory { +<# +.SYNOPSIS + This item creates a directory if it does not exist. + +.PARAMETER FilePath + [string] The appropriate path +#> + [CmdletBinding()] + [OutputType([System.IO.FileInfo])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [Alias('FileName')] + [Alias('File')] + [Alias('Name')] + [Alias('FilePath')] + [string]$Path + ) + + if (!(Test-Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force + } +} + +New-Alias -Name New-Folder -Value New-Directory \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-File.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-File.ps1 new file mode 100644 index 0000000..d906745 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-File.ps1 @@ -0,0 +1,22 @@ +function New-File { +<# +.SYNOPSIS + This item creates a file if it does not exist. + +.PARAMETER FilePath + [string] The appropriate path +#> + [CmdletBinding()] + [OutputType([System.IO.FileInfo])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [Alias('FileName')] + [Alias('File')] + [Alias('Name')] + [string]$FilePath + ) + + if (!(Test-Path $FilePath)) { + New-Item -Path $FilePath -ItemType File -Force + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-JiraDevTicket.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-JiraDevTicket.ps1 new file mode 100644 index 0000000..a7ebf48 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-JiraDevTicket.ps1 @@ -0,0 +1,338 @@ +function New-JiraDevTicket { + param ( + [Parameter(Mandatory = $true)] + [string]$Summary, + [Parameter(Mandatory = $true)] + [string]$Body, + [Parameter(Mandatory = $true)] + [ValidateSet('Account Intelligence','Account Services','Analytics Visualization','API','Business Payments','Business Services','Carbon','Card Experience','Core Custom','Core Standard','Data Engine','Ignite','MMA','MMC','Mobile Platform','Native Mobile Android','Native Mobile iOS','PlatformB','SDK','Sidekick','Steam','Sustained Development','Vanguard','WidgetsD','AI','AS','AV','BIZPAY','BIZSVC','CE','CORECUSTOM','CORE','DE','Mobile','Android','iOS','PLAT','COM','ARCH','BIZDATABILLING','WIDD','TI','CS','FS')] + [string]$Team, + [Parameter(Mandatory = $false)] + [string]$Labels, + [Parameter(Mandatory = $false)] + [ValidateSet('ChangeRequest','ContractItem','PlatformImprovement','ReleaseDelivery','StrategicRoadmap','Support')] + [string]$IssueSource = 'PlatformImprovement', + [Parameter(Mandatory = $false)] + [ValidateSet('Analytics','Architecture','AutomationTooling','CodeCleanup','Componentization','DevStandards','Documentation','Logging','PerformanceOptimization','Security','Strategic','Supportability','UnitTesting')] + [string]$RequestType = 'DevStandards', + [Parameter(Mandatory = $false)] + [ValidateSet('None','Dev','QA','Staging','Production')] + [string]$EnvironmentFoundIn = 'None', + [Parameter(Mandatory = $false)] + [ValidateSet('AccountsAggregation','AccountsCertificateMaturityOptions','AccountsCheckImaging','AccountsNative','AccountsQuickenandQuickbooks','AccountsTransactionDispute','AccountsTransactionEnrichmentTDE','Accounts','API','ApplicationsWidget','Benefits','BillPay','Billing','Budgets','BusinessACH','BusinessAdmin','BusinessReports','BusinessRiskMitigation','BusinessWires','CalculatorCalendar','CardManagementDigitalCards','CardManagement','CardlessCash','Chat','Content','ConversationalBanking','CoreCorelationKeystone','CoreCUFX','CoreD+H/HFSPhoenixXM','CoreD+H/HFSUltradata','CoreDataeXchange','CoreDesertESB','CoreDMI','CoreEpisysSymConnect','CoreEpisysSymXchange','CoreFICSRTA','CoreFirstTech','CoreFISBase2000','CoreFISCreditCards','CoreFISHorizonCodeConnect','CoreFISMISER','CoreFISPaymentsOne','CoreFiservCCM','CoreFiservCoACCM','CoreFiservDNACoreAPI','CoreFiservDNAIRB','CoreFiservOmaha','CoreFiservXP2Apex','CoreFiserv/ITICoA','CoreiPower','CoreMortgageBatch','CoreOCCUESB','CoreOrion','CoreOTSESB','CoreSalesforce','CoreSandiaLabsESB','CoreSilverlake','CoreSpectrumPathways','CoreSTCUESB','CoreThoughtMachine','CoreTMGODS','CourtesyPay','CreditScoreFICO','CreditScoreSavvyMoney','Cryptocurrency','CustomBaxter','CustomCivicRewards','CustomFirstFlorida','CustomIdahoCentral','CustomMACU','CustomMission','CustomOregonCommunityRewards','CustomPatelco','CustomSandiaLabs','CustomSPIRECharity','CustomUSF','Dashboard','DigitalID','DraftServices','eDocuments','FinancialWellness','Flux','Ignite','InstantAccountVerificationIAV','InternalTools','Investments','Iris','LoanCoupon','Locations','MessageCenter','MultiLanguage','N/A','Navigation','NotificationDeliveryPush','NotificationDelivery','OverdraftProtection','P2PZelle','PayrollDistribution','Platform','QuickApply','RDC','RetailWires','SavingsGoals','SDK','SecurityBioCatch','SecurityCyxteraDetectTA','SecurityIntegratedAdminAuthentication','SecurityTokens','Security','SecurityAPI','SharedAccess','SkipAPay','SSOBillPay','SSOsAccountOpening','SSOsAccounts','SSOsBenefits','SSOsBusinessAdmin','SSOsCardManagement','SSOsDocumentRepository','SSOsDraftServices','SSOseDocs','SSOsP2P','SSOsRDC','SSOsSkipaPay','Themes','TransactionDownload','Transfers','UserManagement','UserSentiment','UserServices','WebAnalytics')] + [string]$Component = 'Platform', + [Parameter(Mandatory = $false)] + [ValidatePattern('^[A-Z]+-[0-9]+$')] + [string]$EpicTicketId + ) + # $issueSourceValue = Get-IssueSourceValue -Value + $issueSourceValue = switch ($IssueSource) { + 'ChangeRequest' { '12706' } + 'ContractItem' { '12705' } + 'PlatformImprovement' { '11004' } + 'ReleaseDelivery' { '13405' } + 'StrategicRoadmap' { '11005' } + 'Support' { '11006' } + } + + $requestTypeValue = switch ($RequestType) { + 'Analytics' { 24101 } + 'Architecture' { 24302 } + 'AutomationTooling' { 24303 } + 'CodeCleanup' { 24103 } + 'Componentization' { 24104 } + 'DevStandards' { 24105 } + 'Documentation' { 24106 } + 'Logging' { 24107 } + 'PerformanceOptimization' { 24108 } + 'Security' { 24304 } + 'Strategic' { 24109 } + 'Supportability' { 24102 } + 'UnitTesting' { 24110 } + } + + $environmentFoundInValue = switch ($EnvironmentFoundIn) { + 'Dev' { '18241' } + 'QA' { '18242' } + 'Staging' { '18243' } + 'Production' { '12006' } + default { $null } + } + + $componentsValue = switch ($Component) { + 'AccountsAggregation' { '20805' } + 'AccountsCertificateMaturityOptions' { '28967' } + 'AccountsCheckImaging' { '14125' } + 'AccountsNative' { '24203' } + 'AccountsQuickenandQuickbooks' { '28966' } + 'AccountsTransactionDispute' { '19906' } + 'AccountsTransactionEnrichmentTDE' { '22000' } + 'Accounts' { '22415' } + 'API' { '23018' } + 'ApplicationsWidget' { '14445' } + 'Benefits' { '23744' } + 'BillPay' { '19601' } + 'Billing' { '22004' } + 'Budgets' { '14404' } + 'BusinessACH' { '16222' } + 'BusinessAdmin' { '23016' } + 'BusinessReports' { '23017' } + 'BusinessRiskMitigation' { '28970' } + 'BusinessWires' { '17214' } + 'CalculatorCalendar' { '14609' } + 'CardManagementDigitalCards' { '88273' } + 'CardManagement' { '19815' } + 'CardlessCash' { '61578' } + 'Chat' { '14170' } + 'Content' { '19904' } + 'ConversationalBanking' { '28963' } + 'CoreCorelationKeystone' { '14190' } + 'CoreCUFX' { '24403' } + 'CoreD+H/HFSPhoenixXM' { '14187' } + 'CoreD+H/HFSUltradata' { '14185' } + 'CoreDataeXchange' { '19981' } + 'CoreDesertESB' { '18004' } + 'CoreDMI' { '29567' } + 'CoreEpisysSymConnect' { '22420' } + 'CoreEpisysSymXchange' { '23762' } + 'CoreFICSRTA' { '21403' } + 'CoreFirstTech' { '21700' } + 'CoreFISBase2000' { '61925' } + 'CoreFISCreditCards' { '14516' } + 'CoreFISHorizonCodeConnect' { '93400' } + 'CoreFISMISER' { '15816' } + 'CoreFISPaymentsOne' { '89590' } + 'CoreFiservCCM' { '23106' } + 'CoreFiservCoACCM' { '30667' } + 'CoreFiservDNACoreAPI' { '18908' } + 'CoreFiservDNAIRB' { '14188' } + 'CoreFiservOmaha' { '71317' } + 'CoreFiservXP2Apex' { '14186' } + 'CoreFiserv/ITICoA' { '23765' } + 'CoreiPower' { '14604' } + 'CoreMortgageBatch' { '14249' } + 'CoreOCCUESB' { '20408' } + 'CoreOrion' { '27602' } + 'CoreOTSESB' { '19982' } + 'CoreSalesforce' { '61397' } + 'CoreSandiaLabsESB' { '19300' } + 'CoreSilverlake' { '19980' } + 'CoreSpectrumPathways' { '14189' } + 'CoreSTCUESB' { '23774' } + 'CoreThoughtMachine' { '83561' } + 'CoreTMGODS' { '21404' } + 'CourtesyPay' { '21608' } + 'CreditScoreFICO' { '23400' } + 'CreditScoreSavvyMoney' { '28000' } + 'Cryptocurrency' { '73700' } + 'CustomBaxter' { '23107' } + 'CustomCivicRewards' { '24100' } + 'CustomFirstFlorida' { '23404' } + 'CustomIdahoCentral' { '22911' } + 'CustomMACU' { '23104' } + 'CustomMission' { '24404' } + 'CustomOregonCommunityRewards' { '61393' } + 'CustomPatelco' { '20900' } + 'CustomSandiaLabs' { '21405' } + 'CustomSPIRECharity' { '30669' } + 'CustomUSF' { '24306' } + 'Dashboard' { '23734' } + 'DigitalID' { '61394' } + 'DraftServices' { '14345' } + 'eDocuments' { '14194' } + 'FinancialWellness' { '42271' } + 'Flux' { '23702' } + 'Ignite' { '81301' } + 'InstantAccountVerificationIAV' { '78025' } + 'InternalTools' { '19901' } + 'Investments' { '14500' } + 'Iris' { '24304' } + 'LoanCoupon' { '16501' } + 'Locations' { '14511' } + 'MessageCenter' { '23737' } + 'MultiLanguage' { '28962' } + 'N/A' { '24209' } + 'Navigation' { '83560' } + 'NotificationDeliveryPush' { '15600' } + 'NotificationDelivery' { '20022' } + 'OverdraftProtection' { '19819' } + 'P2PZelle' { '14256' } + 'PayrollDistribution' { '20803' } + 'Platform' { '13607' } + 'QuickApply' { '19792' } + 'RDC' { '23117' } + 'RetailWires' { '20505' } + 'SavingsGoals' { '17217' } + 'SDK' { '15282' } + 'SecurityBioCatch' { '80984' } + 'SecurityCyxteraDetectTA' { '28964' } + 'SecurityIntegratedAdminAuthentication' { '28969' } + 'SecurityTokens' { '28965' } + 'Security' { '14506' } + 'SecurityAPI' { '28961' } + 'SharedAccess' { '23303' } + 'SkipAPay' { '21606' } + 'SSOBillPay' { '41803' } + 'SSOsAccountOpening' { '23105' } + 'SSOsAccounts' { '27400' } + 'SSOsBenefits' { '23014' } + 'SSOsBusinessAdmin' { '23703' } + 'SSOsCardManagement' { '23407' } + 'SSOsDocumentRepository' { '67201' } + 'SSOsDraftServices' { '14176' } + 'SSOseDocs' { '14198' } + 'SSOsP2P' { '14257' } + 'SSOsRDC' { '18303' } + 'SSOsSkipaPay' { '23405' } + 'Themes' { '14322' } + 'TransactionDownload' { '21609' } + 'Transfers' { '22900' } + 'UserManagement' { '20807' } + 'UserSentiment' { '26500' } + 'UserServices' { '22500' } + 'WebAnalytics' { '81620' } + } + + $teamTranslation = switch ($Team) { + 'AI' { 'Account Intelligence' } + 'AS' { 'Account Services' } + 'AV' { 'Analytics Visualization' } + 'BIZPAY' { 'Business Payments' } + 'BIZSVC' { 'Business Services' } + 'CE' { 'Card Experience' } + 'CORECUSTOM' { 'Core Custom' } + 'CORE' { 'Core Standard' } + 'DE' { 'Data Engine' } + 'Mobile' { 'Mobile Platform' } + 'Android' { 'Native Mobile Android' } + 'iOS' { 'Native Mobile iOS' } + 'PLAT' { 'PlatformB' } + 'COM' { 'Vanguard' } + 'ARCH' { 'Vanguard' } + 'BIZDATABILLING' { 'WidgetsD' } + 'WIDD' { 'Carbon' } + 'TI' { 'Implementation Services' } + 'FS' { 'Fraud & Security' } + 'CS' { 'Customer Service' } + default { $Team } + } + + $teamValue = switch ($teamTranslation) { + 'Account Intelligence' { 60 } + 'Account Services' { '133' } + 'Analytics Visualization' { '124' } + 'API' { '52' } + 'Business Payments' { '115' } + 'Business Services' { '116' } + 'Carbon' { '96' } + 'Card Experience' { '134' } + 'Core Custom' { '63' } + 'Core Standard' { '64' } + 'Customer Service' { '136' } + 'Data Engine' { '123' } + 'Fraud & Security' { '137' } + 'Ignite' { '148' } + 'Implementation Services' { '34' } + 'MMA' { '47' } + 'MMC' { '85' } + 'Mobile Platform' { '132' } + 'Native Mobile Android' { '86' } + 'Native Mobile iOS' { '24' } + 'PlatformB' { '66' } + 'SDK' { '53' } + 'Sidekick' { '126' } + 'Steam' { '131' } + 'Sustained Development' { '19' } + 'Vanguard' { '79' } + 'WidgetsD' { '45' } + } + + $issueSourceField = 'customfield_11200' + $requestTypeField = 'customfield_17000' + $environmentFoundInField = 'customfield_12001' + $teamsField = 'customfield_10108' + $epicField = 'customfield_10405' + $customerField = 'customfield_12005' + $naCustomerId = '14408' # N/A + $devProjectId = '11000' # This means "DEV" project + $devIssueTypeStory = '10601' # This means "Story" + + $message = @{ + fields = @{ + project = @{ + id = $devProjectId + } + issuetype = @{ + id = $devIssueTypeStory + } + summary = $Summary + # According to the documentation, I can just encode text here (possibly replacing newlines with `n) + description = $Body + components = @( + @{ + id = $componentsValue + } + ) + $customerField = @( + @{ + id = $naCustomerId + } + ) + $issueSourceField = @{ + value = 'Platform Improvement' + } + $requestTypeField = @{ + value = 'Componentization' + } + $teamsField = $teamTranslation + <# @{ + id = $teamValue + key = $teamValue + # key = $teamTranslation + }#> + } + } + + if ($null -ne $environmentFoundInValue) { + $message.fields[$environmentFoundInField] = $environmentFoundInValue + } + + if (![string]::IsNullOrWhiteSpace($Labels)) { + $message.fields['labels'] = @( $Labels -split ',' ) + } + + if (![string]::IsNullOrWhiteSpace($EpicTicketId)) { + $message.fields[$epicField] = $EpicTicketId + } + + $payload = (ConvertTo-Json $message -Depth 10 -Compress) + Write-Host $payload + + + $url = (Get-JiraBaseUrl) + $headers = (Get-JiraBearerTokenAuthWebHeader) + $headers["Content-Type"] = "application/json" + $headers["Accept"] = "application/json" + + $jiraUrlMetaCreateFields = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue/") + + $arguments = @{ + Headers = $headers + Uri = $jiraUrlMetaCreateFields + Method = 'POST' + Body = $payload + } + + try { + $response = Invoke-RestMethod @arguments + return $response + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + return + } +} + +<# + +#> \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-JiraTicket.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-JiraTicket.ps1 new file mode 100644 index 0000000..748d17d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-JiraTicket.ps1 @@ -0,0 +1,95 @@ +function New-JiraTicket { +<# +.SYNOPSIS + Create a new Jira ticket (issue) + +.PARAMETER TicketNumber + The Jira ticket number + +.PARAMETER Message + The content of the message + +.PARAMETER Credential + Optional. Credentials to talk to Jira. Expected to be stored in a user-environment-variable for the default case. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [string]$Project, + [Parameter(Mandatory = $true)] + [string]$Type, + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [PSCredential]$Credential + ) + + $logLead = (Get-LogLeadName) + + if ($null -eq $Credential) { + $Credential = (Get-CredentialFromEnvironmentVariables) + } + + if ($null -eq $Credential) { + Write-Error "$logLead : Can not talk to Jira without credentials. Returning." + return + } + + $headers = (Get-BasicAuthWebHeader -Credential $Credential) + $headers["Content-Type"] = "application/json" + + $url = (Get-JiraBaseUrl) + # Use this URL to ensure the ticket number as provided exists + $jiraUrlTicket = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue") + + $arguments = @{ + UseBasicParsing = $true + Headers = $headers + Uri = $jiraUrlTicket + Method = 'POST' + } + + try { + $response = Invoke-RestMethod @arguments + if (![string]::IsNullOrWhiteSpace($response.key)) { + # In case the ticket has been moved, let's goto the right value + $TicketNumber = $response.key + } + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + Write-Error "$logLead : Could not verify ticket. Ensure proper credentials and try again, or verify the ticket number is correct." + return + } + + # Use this URL to post the comment to the body + $jiraUrlComment = (Join-UrlComponents -BaseUrl $url -Path "/rest/api/latest/issue") + + $body = @{ + body = $Message + } + + $arguments = @{ + UseBasicParsing = $true + Headers = $headers + Uri = $jiraUrlComment + Body = (ConvertTo-Json $body -Depth 10) + Method = 'Post' + } + + try { + $response = (Invoke-RestMethod @arguments) + if (![string]::IsNullOrWhiteSpace($response.key)) { + # In case the ticket has been moved, let's goto the right value + $TicketNumber = $response.key + } + } catch { + Write-Host (Get-LastWebRequestErrorText) + Write-ErrorObject -ErrorItem $PSItem + Write-Error "$logLead : Could not add comment to ticket. Ensure proper credentials and try again, or verify the ticket number is correct." + return + } +} + +Set-Alias -Name New-JiraIssue -Value New-JiraTicket \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/New-List.ps1 b/Modules/Cole.PowerShell.Developer/Public/New-List.ps1 new file mode 100644 index 0000000..7962c40 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/New-List.ps1 @@ -0,0 +1,9 @@ +function New-List { +<# +.SYNOPSIS + Returns a new ArrayList +#> + [CmdletBinding()] + param() + (New-Object -TypeName "System.Collections.ArrayList") +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Open-TeamCityBlock.ps1 b/Modules/Cole.PowerShell.Developer/Public/Open-TeamCityBlock.ps1 new file mode 100644 index 0000000..da944cf --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Open-TeamCityBlock.ps1 @@ -0,0 +1,69 @@ +function Open-TeamCityBlock { +<# +.SYNOPSIS + Open a TeamCity block with a header and description. Useful for collapsing content. + +.DESCRIPTION + Used to write a handy marker for content collapsing. + Note that TeamCity does not honor the name of the closed block as a developer might, so the first close that occurs after any open will close that opening block. + This is tricky to troubleshoot and will make your life frustrating when you do not manage your opening and closing as tightly as you can. + Using the pair of named functions makes it much easier to see that you have indeed opened and closed the block as expected. + +.PARAMETER Name + The name of the block being opened. + Think "collapsed" + +.PARAMETER Description + The description to show in the UI for more detail. + Think "expanded" + +.OUTPUTS + Returns an object that can be used in pipeline formation to close the block later + +.LINK + Close-TeamCityBlock + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock -Block $block + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock @block + +.EXAMPLE + $block = Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + $block | Close-TeamCityBlock + +.EXAMPLE + Open-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" + Close-TeamCityBlock -Name "some Name" -Description "This is a long description that indicates what we are doing here" +Note that this example will emit a block-opened object into the pipeline +#> + [CmdletBinding()] + [OutputType([object])] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Description + ) + + $sanitizedName = ConvertTo-SafeTeamCityMessage -InputText $Name + $sanitizedDescription = ConvertTo-SafeTeamCityMessage -InputText $Description + + if (Test-IsTeamCityProcess) { + Write-Host "##teamcity[blockOpened name='$sanitizedName' description='$sanitizedDescription']" + } else { + Write-Host "$logLead : OpenBlock $sanitizedName : $sanitizedDescription" + } + + # Note that we _could_ *only* return this if an assignment was requested. Ex: $MyInvocation.Line -contains '=' + # The way it is written, if it is only invoked on the call alone, we'll get a dump of what was started into the output stream + # This could be useful in some scenarios, for debugging + return @{ + Name = $sanitizedName + Description = $sanitizedDescription + WasSanitized = $true + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Optimize-CIPackageInstallList.ps1 b/Modules/Cole.PowerShell.Developer/Public/Optimize-CIPackageInstallList.ps1 new file mode 100644 index 0000000..3875283 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Optimize-CIPackageInstallList.ps1 @@ -0,0 +1,133 @@ +function Optimize-CIPackageInstallList { +<# +.SYNOPSIS + Used to take an RM provided list of packages and re-categorize it to help CIx installs. + Specifically this will remove certain packages from being installed to help speed up install time, and reduce conflicts + This function will place contents on your clipboard, then prompt you to continue so you can get the next set. + +.PARAMETER Path + [string] Specify a file where you copy-paste the contents from Jira, because automation should be consistent. + +.PARAMETER Contents + [string[]] Specify a string array containing the elements you want recategorized. Should be a copy-paste of the contents from Jira, because the automation there should be consistent. +#> + [CmdletBinding(DefaultParameterSetName = 'Path')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + [string]$Path, + + [Parameter(Mandatory = $true, ParameterSetName = 'Contents')] + [string[]]$Contents + ) + + $logLead = (Get-LogLeadName) + + if ($PSCmdlet.ParameterSetName -eq 'Path') { + $Contents = (Get-Content -Path $Path) + } + + $colesRejectedPackageList = @( + 'billpayproviders' + 'remotedepositproviders' + 'rdc' + 'symconnect' + 'cardmanagementproviders' + ) + + $appInstalls = @() + $appUninstalls = @() + $webInstalls = @() + $webUninstalls = @() + + $inAppInstalls = $false + $inAppUninstalls = $false + $inWebInstalls = $false + $inWebUninstalls = $false + foreach($line in $Contents) { + $line = $line.Trim() + if ([string]::IsNullOrWhiteSpace($line)) { continue } + + if ($line.StartsWith('Uninstall the following elements on the App Tier')) { + $inAppInstalls = $false + $inAppUninstalls = $true + $inWebInstalls = $false + $inWebUninstalls = $false + continue + } elseif ($line.StartsWith('Install the following elements on the App Tier')) { + $inAppInstalls = $true + $inAppUninstalls = $false + $inWebInstalls = $false + $inWebUninstalls = $false + continue + } elseif ($line.StartsWith('Uninstall the following elements on the Web Tier')) { + $inAppInstalls = $false + $inAppUninstalls = $false + $inWebInstalls = $false + $inWebUninstalls = $true + continue + } elseif ($line.StartsWith('Install the following elements on the Web Tier')) { + $inAppInstalls = $false + $inAppUninstalls = $false + $inWebInstalls = $true + $inWebUninstalls = $false + continue + } elseif ($line.StartsWith('Changes Include:')) { + # done! + break + } elseif ($inAppInstalls) { + $badPattern = $false + $lowerLine = $line.ToLower() + foreach($entry in $colesRejectedPackageList) { + if (!$badPattern -and ($lowerLine.IndexOf($entry) -gt -1)) { + $badPattern = $true + } + } + if ($badPattern) { + $appUninstalls += ($line -split ' ')[0] + } else { + $appInstalls += $line + } + } elseif ($inAppUninstalls) { + $appUninstalls += ($line -split ' ')[0] + } elseif ($inWebInstalls) { + $badPattern = $false + $lowerLine = $line.ToLower() + foreach($entry in $colesRejectedPackageList) { + if (!$badPattern -and ($lowerLine.IndexOf($entry) -gt -1)) { + $badPattern = $true + } + } + if ($badPattern) { + $webUninstalls += ($line -split ' ')[0] + } else { + $webInstalls += $line + } + } elseif ($inWebUninstalls) { + $webUninstalls += ($line -split ' ')[0] + } else { + # + } + } + + $appInstalls = $appInstalls | Sort-Object -Unique + $appUninstalls = $appUninstalls | Sort-Object -Unique + $webInstalls = $webInstalls | Sort-Object -Unique + $webUninstalls = $webUninstalls | Sort-Object -Unique + + Read-Host "Press any key to copy to clipboard the appInstall block" + ($appInstalls -join "`n") | Set-Clipboard + + Read-Host "Press any key to copy to clipboard the appUninstall block" + $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + ($appUninstalls -join "`n") | Set-Clipboard + + Read-Host "Press any key to copy to clipboard the webInstall block" + $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + ($webInstalls -join "`n") | Set-Clipboard + + Read-Host "Press any key to copy to clipboard the webUninstall block" + $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + ($webUninstalls -join "`n") | Set-Clipboard + + Write-Host "Run finished" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Publish-TeamCityArtifact.ps1 b/Modules/Cole.PowerShell.Developer/Public/Publish-TeamCityArtifact.ps1 new file mode 100644 index 0000000..faad203 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Publish-TeamCityArtifact.ps1 @@ -0,0 +1,41 @@ +function Publish-TeamCityArtifact { +<# +.SYNOPSIS + Publish an Artifact to the TeamCity job. This should be a file in a workspace folder that TeamCity can path to + +.DESCRIPTION + The artifact gets published on the job so it can be viewed long-term. An example use-case would be publishing a json file generated during a build-step. + The artifact _may_ get stored on S3 by TeamCity automatically, but this function does not explicitly store the file on S3. + The artifact _may_ get purged by TeamCity processes automatically, if you need this artifact to be retained indefinitely you should use an alternate storage mechanism. + The function will attempt to test that the path exists. It will warn if the path does not exist/point to a valid file. + +.PARAMETER Path + The path to the object +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $logLead = Get-LogLeadName + + if (-not (Test-Path -Path $Path)) { + Write-Warning "$logLead : Could not resolve the path passed in, is it valid? [$Path]" + } + + $charactersToEscape = @( "|", "'", "’", "[", "]") + foreach ($character in $charactersToEscape) { + if ($Path -contains $character) { + Write-Warning "$logLead : Your path [$Path] contains a potentially invalid character. Pleae confirm it will work as expected. Character: $character" + } + } + + if (Test-IsTeamCityProcess) { + Write-Host "##teamcity[publishArtifacts '$Path']" + } else { + Write-Host "$logLead : TeamCity should publish the artifact at [$Path]" + } +} + diff --git a/Modules/Cole.PowerShell.Developer/Public/Read-GitConfig.ps1 b/Modules/Cole.PowerShell.Developer/Public/Read-GitConfig.ps1 new file mode 100644 index 0000000..2800c3d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Read-GitConfig.ps1 @@ -0,0 +1,78 @@ +function Read-GitConfig { +<# +.SYNOPSIS + Naively read the git config. This function is not great. It basically sort of works. + +.PARAMETER Path + [string] The path of the git config to read +#> + param( + $Path + ) + + $return = @{} + $lines = Get-Content -Path $Path + + $currentObject = @{} + $currentArray = @() + $currentObjectDirty = $false + $currentKey = $null + foreach($line in $lines) { + # ignore comments + $line = ($line -split '#' -split ';')[0] + # remove whitespace + $line = $line.Trim() + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + + if ($line.StartsWith('[')) { + if ($currentObjectDirty) { + if ($null -eq $return.$currentKey) { + $return.$currentKey = @() + } + if ([string]::IsNullOrWhiteSpace($currentObject.name)) { + $return.$currentKey += $currentArray + } else { + $return.$currentKey += $currentObject + } + $currentObject = @{} + $currentArray = @() + $currentKey = $null + $currentObjectDirty = $false + } + $line = $line -replace '\[','' -replace '\]','' + $firstSpace = $line.IndexOf(' ') + $currentKey = $line + $name = $null + if ($firstSpace -gt -1) { + $currentKey = $line.Substring(0,$firstSpace).Trim() + $name = $line.Substring($firstSpace + 1).Trim() -replace '"','' + } + + if (![string]::IsNullOrWhiteSpace($name)) { + $currentObject.name = $name + } + $currentObjectDirty = $true + } else { + Write-Host $line + $firstSplit = $line.IndexOf('=') + if ($firstSplit -gt -1) { + $name = $line.Substring(0,$firstSplit).Trim() + if ([string]::IsNullOrWhiteSpace($name)) { + Write-Error "no variable name found" + continue + } + $value = $line.Substring($firstSplit + 1).Trim() -replace '"','' + $currentArray += @{ $name = $value } + $currentObject.$name = $value + $currentObjectDirty = $true + } else { + $currentArray += $line + $currentObjectDirty = $true + } + } + } + + return $return +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Read-StreamAsString.ps1 b/Modules/Cole.PowerShell.Developer/Public/Read-StreamAsString.ps1 new file mode 100644 index 0000000..53d4c06 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Read-StreamAsString.ps1 @@ -0,0 +1,29 @@ +function Read-StreamAsString { +<# +.SYNOPSIS + Reads a given stream as a string. This is just a wrapper on a streamreader + +.PARAMETER Stream + [System.IO.Stream] Must be an open, readable stream, preferably at position 0 already. +#> + param ( + [System.IO.Stream]$Stream + ) + + $response = "[[Stream was not readable]]" + + # Reset to the beginning in case it isn't at the beginning + # This is likely to be the largest source of errors + # The other is if the stream is already closed + if ($Stream.CanSeek) { + $Stream.Position = 0 + } + if ($Stream.Position -eq 0) { + $streamReader = [System.IO.StreamReader]::new($Stream) + $response = $streamReader.ReadToEnd() + $streamReader.Close() + $Stream.Close() + } + + return $response +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Read-Xml.ps1 b/Modules/Cole.PowerShell.Developer/Public/Read-Xml.ps1 new file mode 100644 index 0000000..8c08a7f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Read-Xml.ps1 @@ -0,0 +1,204 @@ +function Read-Xml { + param ( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + + $logLead = (Get-LogLeadName) + + if (!(Test-Path -Path $Path)) { + throw "$logLead : Not able to resolve [$Path]" + } + + function parseTag { + param ( + [string]$tagContent + ) + + # content should look like + # tagName attribute-name="attribute-value" + # or + # tagName attribute-name='attribute-value' + # but because we aren't assholes, we can use both ' and " and just look for the next matching non-escaped one + + $charEscapedSingleQuote = [char]([byte]17) + $charEscapedDoubleQuote = [char]([byte]18) + $charEscapedStringSpace = [char]([byte]19) + $tagContent = $tagContent.Replace('\"',$charEscapedDoubleQuote).Replace("\'",$charEscapedSingleQuote) + + $tagContentBytes = [System.Text.Encoding]::UTF8.GetBytes($tagContent) + + $node = @{ Name = ''; } + $tagContentLength = $tagContent.Length + $currentToken = '' + $inString = $false + $inSingleString = $false + $inDoubleString = $false + for($i = 0; $i -lt $tagContentLength; $i++) { + $char = $tagContent[$i] + if ($inString) { + if (($char -eq '"') -and $inDoubleString) { + # we found the end of the string + $inString = $false + $inDoubleString = $false + } elseif (($char -eq "'") -and $inSingleString) { + # we found the end of the string + $inString = $false + $inSingleString = $false + } elseif ($char -eq ' ') { + # replace the space so we can quickly token parse our strings + $tagContentBytes[$i] = 19 # this matches $charEscapedStringSpace above + } elseif ($char -eq '"') { + $tagContentBytes[$i] = 18 # this matches $charEscapedDoubleQuote above + } elseif ($char -eq "'") { + $tagContentBytes[$i] = 17 # this matches $charEscapedSingleQuote above + } + } elseif ($char -eq '"') { + $inString = $true + $inDoubleString = $true + } elseif ($char -eq "'") { + $inString = $true + $inSingleString = $true + } + } + + $tagContent = [System.Text.Encoding]::UTF8.GetString($tagContentBytes) + + # Now handle the case of + # It should be + + while (($tagContent.IndexOf(' =') -gt -1) -or ($tagContent.IndexOf('= ') -gt -1)) { + $tagContent = $tagContent.Replace(' =','=').Replace('= ','=') + } + + # now $tagContent has been escaped, so we can split on spaces, then equals, then remove quotes + $splits = $tagContent -split ' ' + $splitCount = $splits.Count + $node.Name = $splits[0] + if ($splitCount -gt 1) { + $node.Attributes = @{} + } + for ($i = 1; $i -le $splitCount; $i++) { + $attributeRawValue = $splits[$i] + if ([string]::IsNullOrWhiteSpace($attributeRawValue)) { + # can't parse empty spaces, sadly :D + continue + } + $attributeEqualsIndex = $attributeRawValue.IndexOf('=') + $name = $attributeRawValue + $value = $attributeRawValue + if ($attributeEqualsIndex -eq -1) { + # the attribute stands alone, so we set it equal to itself (above) + } else { + $name = $attributeRawValue.Substring(0, $attributeEqualsIndex) + $value = $attributeRawValue.Substring($attributeEqualsIndex + 1).Replace('"','').Replace("'","").Replace($charEscapedStringSpace,' ').Replace($charEscapedDoubleQuote,'"').Replace($charEscapedSingleQuote,"'") + } + $node.Attributes.$name = $value + } + return $node.Name, $node.Attributes + } + + $rawcontent = (Get-Content -Raw -Path $Path) + + function parseNodes { + param ( + $content + ) + + $parsedElements = @{} + + $currentTag = $null + $beginTag = $false + $contentLength = $content.Length + for ($i = 0; $i -lt $contentLength; $i++) { + $char = $content[$i] + if ($char -eq '<') { + if ($content[$i+1] -eq '?') { + # We are in the xml chunk + $skipTo = $content.IndexOf('?>',$i+1) + $i = $skipTo + 1 + continue + } elseif ($content[$i+1] -eq '!') { + # check if we are in CDATA mode, so we can skip to the end with the content in our node + Write-Host $content.Substring($i+1,7) + if ($content.Substring($i+1,7) -eq "!CDATA[") { + # We are in a CDATA chunk and can skip ahead to the end of it which is the next occurrence of ]]> + # We assume that only someone who truly hates us would do a nested CDATA block, cos of our limited scope of audience + # In a full fledged parser we would use a stack to track that we were in X + $skipTo = $content.IndexOf(']]>',$i+1) + $i = $skipTo + 1 + } + } else { + $beginTag = $true + $currentTag = '' + } + } elseif ($char -eq '>') { + $beginTag = $false + if ($currentTag.Length -eq 0) { + throw "$logLead : Found an empty or invalid tag at [$i]" + } + $isSelfClosing = $false + if ($currentTag.EndsWith('/')) { + $isSelfClosing = $true + $currentTag = $currentTag.Substring(0,$currentTag.Length - 1) + } + $parsedTag,$nodeSet = (parseTag $currentTag) + + $foundNodes = $null + $innerText = $null + + # we hit the close tag, so let's find the end-tag of our current tag, unless the previous character was a / (thus forming /> or a self-closing tag) + if ($isSelfClosing) { + # don't look for the end-tag + } else { + # look for the end-tag + $closingTag = "" + $closingTagIndex = $content.IndexOf($closingTag,$i) + + if ($closingTagIndex -eq -1) { + Write-Host $content.Substring($i) + throw "$logLead : Couldn't find a closing tag for [$($parsedTag)] starting at or around [$i]" + } + $endIndex = $closingTagIndex + $closingTag.Length + + $innerContent = $content.Substring($i + 1,$closingTagIndex - $i - 1) + if ($innerContent.IndexOf("<$($parsedTag)") -gt -1) { + # We have a case of a recursive tag, where we contain ourselves, so we need to skip past nested same-as-self tags + # Ugh, what a disaster of an edge-case + $lastIndexOfSameTag = $content.LastIndexOf("<$($parsedTag)") + + # Now find the next index of the closing tag from here + # Then find the next index of the closing tag from _that_ place + $closingTagIndex = $content.IndexOf($closingTag,$lastIndexOfSameTag + 1) + + $newClosingTagIndex = $content.IndexOf($closingTag,$closingTagIndex + 1) + + $endIndex = $newClosingTagIndex + $closingTag.Length + $innerContent = $content.Substring($i + 1,$newClosingTagIndex - $i - 1) + } + if (![string]::IsNullOrWhiteSpace($innerContent)) { + if ($innerContent.IndexOf('<') -gt -1) { + $childNodes = parseNodes($innerContent) + if ($null -ne $childNodes) { + $foundNodes = $childNodes + } + } else { + $innerText = @{ Text = $innerContent} + } + } + $i = $endIndex + 1 + } + $parsedElements.$parsedTag = Merge-Objects -Objects $parsedElements.$parsedTag,$nodeSet,$foundNodes,$innerText -DontClobber -DontDeepMerge + } elseif ($beginTag) { + $currentTag += $char + } + } + + return $parsedElements + } + + $capture = parseNodes($rawcontent) + + return $capture +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Remove-AllInvalidComponentInstallerPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Remove-AllInvalidComponentInstallerPath.ps1 new file mode 100644 index 0000000..e81599e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Remove-AllInvalidComponentInstallerPath.ps1 @@ -0,0 +1,18 @@ +function Remove-AllInvalidComponentInstallerPath { +<# +.SYNOPSIS + Due to a legacy issue where a space was inadvertently missing in an installer path, some systems have a bad path present. + This function will remove that if found. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + $alkamiPDPath = (Join-Path -Path $env:ProgramData -ChildPath "Alkami") + $installerPath = (Join-Path -Path $alkamiPDPath -ChildPath "Installer-ChildPath") + + if (Test-Path $installerPath) { + Remove-Item -Path $installerPath -Force + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Remove-DuplicateHostsFileRecords.ps1 b/Modules/Cole.PowerShell.Developer/Public/Remove-DuplicateHostsFileRecords.ps1 new file mode 100644 index 0000000..38845a6 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Remove-DuplicateHostsFileRecords.ps1 @@ -0,0 +1,61 @@ +function Remove-DuplicateHostsFileRecords { +<# +.SYNOPSIS + Removes duplicate entries from the hostfile, preferring the longest comment in the case of duplicate entries + Assumes that a distinct record is IpAddress + Hostname + +.PARAMETER Records + A list of two or more records. An empty array or one that has only one element will be returned as-is. + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Object[]]$Records + ) + + # If we have no records, or only one record, how can we remove duplicates? + if ((Test-IsCollectionNullOrEmpty $Records) -or ($Records.Count -eq 1)) { + return $Records + } + + $hash = @{} + foreach ($record in $Records) { + if ([string]::IsNullOrWhiteSpace($record.IpAddress) -and [string]::IsNullOrWhiteSpace($record.Hostname)) { + continue + } else { + $indexString = "$($record.IpAddress)$($record.Hostname)".ToLower() + $formattedRecord = Format-HostsFileRecord -Record $record + if ($null -eq $hash[$indexString]) { + $hash[$indexString] = $formattedRecord + } + if ($formattedRecord.Length -gt $hash[$indexString].Length) { + $hash[$indexString] = $formattedRecord + } + } + } + + $outputRecords = @() + foreach ($record in $Records) { + if ([string]::IsNullOrWhiteSpace($record.IpAddress) -and [string]::IsNullOrWhiteSpace($record.Hostname)) { + $outputRecords += $record + } else { + $indexString = "$($record.IpAddress)$($record.Hostname)".ToLower() + $formattedRecord = Format-HostsFileRecord -Record $record + + # There should have been a record here, so we must have removed it + if ($null -eq $hash[$indexString]) { + continue + } + + if ($formattedRecord.Length -eq $hash[$indexString].Length) { + $outputRecords += $record + + # Only keep the first one that matches, get rid of all the rest by hitting the above fork + $hash[$indexString] = $null + } + } + } + + return $outputRecords +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Remove-HostsFileEntry.ps1 b/Modules/Cole.PowerShell.Developer/Public/Remove-HostsFileEntry.ps1 new file mode 100644 index 0000000..8c29243 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Remove-HostsFileEntry.ps1 @@ -0,0 +1,67 @@ +function Remove-HostsFileEntry { +<# +.SYNOPSIS + Returns all hosts file entries as a list of objects of the format: + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.PARAMETER IpAddress + The IP Address of the relevant hosts entry + +.PARAMETER Hostname + The hostname of the relevant hosts entry + +.PARAMETER Force + Because some records are marked with the word keep to not be deleted + +.OUTPUTS + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$IpAddress, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Hostname, + [switch]$Force + ) + + $logLead = Get-LogLeadName + + $records = Get-HostsFileAllRecords + + $removedRecords = $records.Where({($_.IpAddress -eq $IpAddress) -and ($_.Hostname -eq $Hostname)}) + foreach ($record in $removedRecords) { + Write-Host "$logLead : Removing: $(Format-HostsFileRecord -Record $record)" + } + $updatedRecords = $records.Where({!(($_.IpAddress -eq $IpAddress) -and ($_.Hostname -eq $Hostname))}) + + $keepRecords = $removedRecords.Where({$_.Keep}) + if (!(Test-IsCollectionNullOrEmpty $keepRecords)) { + if ($Force) { + Write-Warning "$logLead : Skipping the following keep records" + } else { + Write-Warning "$logLead : Found Keep records in the remove-requested records. Re-adding to the updated records list. Use -Force to override." + } + $updatedKeepRecords = @() + foreach ($record in $keepRecords) { + $formattedRecord = Format-HostsFileRecord -Record $record + if ($Force) { + Write-Host "$logLead : Removing, not keeping: [$formattedRecord]" + } else { + Write-Host "$logLead : Keeping: [$formattedRecord]" + $newRecord = @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $formattedRecord; BlankLine = $false; } + $updatedKeepRecords += $newRecord + } + } + $updatedRecords += $updatedKeepRecords + } + + if ($removedRecords.Count -gt 0) { + Write-Host "$logLead : Removing: $removedRecordsCount records" + Save-CompleteHostsFile -Record $updatedRecords + } else { + Write-Warning "$logLead : No records found to remove" + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Remove-OrbHostEntries.ps1 b/Modules/Cole.PowerShell.Developer/Public/Remove-OrbHostEntries.ps1 new file mode 100644 index 0000000..ce3e0fb --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Remove-OrbHostEntries.ps1 @@ -0,0 +1,20 @@ +function Remove-OrbHostEntries { +<# +.SYNOPSIS + Removes all the known ORB entries from the hosts file + +.LINK + Get-KnownDeveloperHostsEntries +#> + [CmdletBinding()] + [OutputType([void])] + param() + + $logLead = Get-LogLeadName + + $knownHostEntries = (Get-KnownDeveloperHostsEntries) + foreach ($entry in $knownHostEntries) { + Write-Host "$logLead : Removing hosts file entry with IpAddress $($entry.IpAddress) and Hostname $($entry.Hostname)" + Remove-HostsFileEntry -IpAddress $entry.IpAddress -Hostname $entry.Hostname + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Remove-TextStyle.ps1 b/Modules/Cole.PowerShell.Developer/Public/Remove-TextStyle.ps1 new file mode 100644 index 0000000..dbfb65a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Remove-TextStyle.ps1 @@ -0,0 +1,12 @@ +function Remove-TextStyle { + param ( + [Parameter(Mandatory = $true)] + $Text + ) + + $esc = [char]27 + + $regex = "$esc" + + return $Text -replace $regex,'' +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Reset-TerminalColor.ps1 b/Modules/Cole.PowerShell.Developer/Public/Reset-TerminalColor.ps1 new file mode 100644 index 0000000..97d4903 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Reset-TerminalColor.ps1 @@ -0,0 +1,9 @@ +function Reset-TerminalColor { + [CmdletBinding()] + param () + + [Console]::ResetColor() +} + +Set-Alias -Name Reset-Color -Value Reset-TerminalColor +Set-Alias -Name Reset-PowerShellColor -Value Reset-TerminalColor \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Restore-AllProviders.ps1 b/Modules/Cole.PowerShell.Developer/Public/Restore-AllProviders.ps1 new file mode 100644 index 0000000..2a04427 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Restore-AllProviders.ps1 @@ -0,0 +1,21 @@ +function Restore-AllProviders { +<# +.SYNOPSIS + This will reinstall all the providers found locally in the chocolatey folder. + This does not upgrade the providers. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $allPackages = (Get-AllInstalledComponentsByType -ComponentType Provider) + + Write-Host "$logLead : Found the following packages to install" + $allPackages | Format-Table @{Name = "Package Name"; Width = 40; Expression={$_.PackageName}},@{Name = "Manifest Path"; Expression={$_.ManifestPath}} + + $installerPath = (Get-ComponentInstallerInstallPath -ComponentType Provider) + Invoke-Parallel2 -Objects @($allPackages.ManifestPath) -Arguments @($installerPath) -Script { param($manifestPath,$installerPath) & $installerPath $manifestPath} +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebApplications.ps1 b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebApplications.ps1 new file mode 100644 index 0000000..ca341f4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebApplications.ps1 @@ -0,0 +1,21 @@ +function Restore-AllWebApplications { +<# +.SYNOPSIS + This will reinstall all the webApplications found locally in the chocolatey folder. + This does not upgrade the webApplications. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $allPackages = (Get-AllInstalledComponentsByType -ComponentType WebApplication) + + Write-Host "$logLead : Found the following packages to install" + $allPackages | Format-Table @{Name = "Package Name"; Width = 40; Expression={$_.PackageName}},@{Name = "Manifest Path"; Expression={$_.ManifestPath}} + + $installerPath = (Get-ComponentInstallerInstallPath -ComponentType WebApplication) + Invoke-Parallel2 -Objects @($allPackages.ManifestPath) -Arguments @($installerPath) -Script { param($manifestPath,$installerPath) & $installerPath $manifestPath} +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebExtensions.ps1 b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebExtensions.ps1 new file mode 100644 index 0000000..c8340e3 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWebExtensions.ps1 @@ -0,0 +1,21 @@ +function Restore-AllWebExtensions { +<# +.SYNOPSIS + This will reinstall all the webExtensions found locally in the chocolatey folder. + This does not upgrade the webExtensions. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $allPackages = (Get-AllInstalledComponentsByType -ComponentType WebExtension) + + Write-Host "$logLead : Found the following packages to install" + $allPackages | Format-Table @{Name = "Package Name"; Width = 40; Expression={$_.PackageName}},@{Name = "Manifest Path"; Expression={$_.ManifestPath}} + + $installerPath = (Get-ComponentInstallerInstallPath -ComponentType WebExtension) + Invoke-Parallel2 -Objects @($allPackages.ManifestPath) -Arguments @($installerPath) -Script { param($manifestPath,$installerPath) & $installerPath $manifestPath} +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Restore-AllWidgets.ps1 b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWidgets.ps1 new file mode 100644 index 0000000..e40b692 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Restore-AllWidgets.ps1 @@ -0,0 +1,21 @@ +function Restore-AllWidgets { +<# +.SYNOPSIS + This will reinstall all the widgets found locally in the chocolatey folder. + This does not upgrade the widgets. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + ) + + $logLead = (Get-LogLeadName) + + $allPackages = (Get-AllInstalledComponentsByType -ComponentType Widget) + + Write-Host "$logLead : Found the following packages to install" + $allPackages | Format-Table @{Name = "Package Name"; Width = 40; Expression={$_.PackageName}},@{Name = "Manifest Path"; Expression={$_.ManifestPath}} + + $installerPath = (Get-ComponentInstallerInstallPath -ComponentType Widget) + Invoke-Parallel2 -Objects @($allPackages.ManifestPath) -Arguments @($installerPath) -Script { param($manifestPath,$installerPath) & $installerPath $manifestPath} +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Save-CompleteHostsFile.ps1 b/Modules/Cole.PowerShell.Developer/Public/Save-CompleteHostsFile.ps1 new file mode 100644 index 0000000..e3737a6 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Save-CompleteHostsFile.ps1 @@ -0,0 +1,37 @@ +function Save-CompleteHostsFile { +<# +.SYNOPSIS + Used to save a list of hosts records to the hosts file. Will overwrite all other information in the file. Use with discretion. + +.PARAMETER Records + A list of records of the format @{ Keep = $false; IpAddress = $null; Hostname = $null; Comment = $null; BlankLine = $false; } + +.PARAMETER RemoveDuplicates + Remove duplicates before saving +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [object[]]$Records, + [switch]$RemoveDuplicates + ) + + $logLead = Get-LogLeadName + + if (Test-IsCollectionNullOrEmpty $Records) { + throw "$logLead : no records found to write to the hosts file" + } + + if (($null -eq $Records.IpAddress) -or ($null -eq $Records.Hostname)) { + throw "$logLead : no records found with ipaddress or hostname, can not continue" + } + + if ($RemoveDuplicates) { + # Reuse variable is frowned upon + $Records = Remove-DuplicateHostsFileRecords -Record $Records + } + + $lines = Format-HostsFileRecord -Record $Records + + $lines | Out-File (Get-HostsFilePath) -Encoding ASCII +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Search-History.ps1 b/Modules/Cole.PowerShell.Developer/Public/Search-History.ps1 new file mode 100644 index 0000000..b4472db --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Search-History.ps1 @@ -0,0 +1,40 @@ +function Search-History { +<# +.SYNOPSIS + Used to look for previous commands in your current or PSReadLine buffers +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Find, + [switch]$Interactive, + [switch]$SkipMultilines, + [switch]$OnlyMultilines, + [switch]$SkipLongLines, + [Parameter()] + [string[]]$AdditionalPath + ) + + $logLead = Get-LogLeadName + + if ($SkipMultilines -and $OnlyMultilines) { + throw "$logLead : You can't specify both skip and only multilines" + } + + if ($Interactive) { + Get-HistoryEntries -SkipMultilines:$SkipMultilines -OnlyMultilines:$OnlyMultilines -SkipLongLines:$SkipLongLines -AdditionalPath $AdditionalPath | ? {$_ -like "*$Find*"} | Get-Unique | more + } else { + $found = @() + $entries = (Get-HistoryEntries -SkipMultilines:$SkipMultilines -OnlyMultilines:$OnlyMultilines -SkipLongLines:$SkipLongLines -AdditionalPath $AdditionalPath) + foreach ($entry in $entries) { + if (($entry -join '').IndexOf($Find) -gt -1) { + if ($found -notcontains $entry.Trim()) { + $found += $entry.Trim() + } + } + } + return $found + } +} + +# New-Alias -Name hist -Value Search-History \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Select-RightSubstringWithPadLeft.ps1 b/Modules/Cole.PowerShell.Developer/Public/Select-RightSubstringWithPadLeft.ps1 new file mode 100644 index 0000000..5d39181 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Select-RightSubstringWithPadLeft.ps1 @@ -0,0 +1,20 @@ +function Select-RightSubstringWithPadLeft { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$String, + [Parameter(Mandatory = $true)] + [int]$Length, + [Parameter(Mandatory = $false)] + $PaddingCharacter = ' ' + ) + + $word = $String + if ($word.Length -ge $Length) { + $word = $word.Substring($word.Length - $Length) + } + if ($Length -gt $word.Length) { + $word = $word.PadLeft($Length, $PaddingCharacter) + } + return $word +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-JiraBearerToken.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-JiraBearerToken.ps1 new file mode 100644 index 0000000..d8ee983 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-JiraBearerToken.ps1 @@ -0,0 +1,9 @@ +function Set-JiraBearerToken { + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $Token + ) + + Set-EnvironmentVariable -Name "JIRA_BEARERTOKEN" -Value $Token -StoreName User +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-LocalCachedAWSProfile.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-LocalCachedAWSProfile.ps1 new file mode 100644 index 0000000..afdad81 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-LocalCachedAWSProfile.ps1 @@ -0,0 +1,31 @@ +function Set-LocalCachedAWSProfile { +<# +.SYNOPSIS + Used to store or change the locally cached AWS profile from the environment store + +.PARAMETER ProfileName + The name of an AWS profile + +.PARAMETER NoValidate + Don't check the local profile configuration to make sure the profile exists +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string]$ProfileName, + [switch]$NoValidate + ) + + $logLead = (Get-LogLeadName) + + if (!$NoValidate) { + $credentials = (Get-LocalConfiguredAWSProfileNames) + if (!$credentials.Contains($ProfileName)) { + throw "$logLead : Can not find the value [$ProfileName] in your local credentials file. Valid values are [$($credentials -join ',')]" + } + Assert-ValidAWSProfileName -ProfileName $ProfileName + } + + Set-EnvironmentVariable -Name "USERCACHE_AWSProfileKey" -Value $ProfileName -StoreName User +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-LocalUserCredential.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-LocalUserCredential.ps1 new file mode 100644 index 0000000..16a197e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-LocalUserCredential.ps1 @@ -0,0 +1,35 @@ +function Set-LocalUserCredential { +<# +.SYNOPSIS + Set the user's credentials in local environment variables + This is mostly useful as a Profile line such as: `$creds = (Get-CredentialFromEnvironmentVariables) + This way a developer can test faster with stored credentials without having to recreate them frequently +#> + param ( + [Parameter(Mandatory=$false)] + [Alias('Password')] + $InputObject, + [Parameter(Mandatory=$false)] + [Alias('Username')] + $User + ) + + $logLead = (Get-LogLeadName) + + $secureString = $null + + if (![string]::IsNullOrWhiteSpace($InputObject)) { + $secureString = (ConvertTo-SecureString $InputObject -AsPlainText -Force) + } else { + $secureString = (Read-Host -Prompt "Please provide a password" -AsSecureString) + } + + if ([string]::IsNullOrWhiteSpace($User)) { + $User = (whoami) + } + + Write-Host "$logLead : Storing credentials for User [$User] in EnvironmentVariables in the User store" + Set-EnvironmentVariable -Name "CREDENTIAL_USERNAME" -Store User -Value $User + Set-EnvironmentVariable -Name "CREDENTIAL_LASTCHANGED" -Store User -Value ([System.DateTime]::Now.ToString()) + Set-EnvironmentVariable -Name "CREDENTIAL_PASSWORD" -Store User -Value (ConvertFrom-SecureString $secureString) +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable.ps1 new file mode 100644 index 0000000..916b188 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable.ps1 @@ -0,0 +1,220 @@ +function Set-PathVariable { + param ( + [Parameter(Mandatory = $true)] + [Alias("Store")] + [Alias("Location")] + [ValidateSet("Process","User","Machine","Any")] + [string]$StoreName, + [Parameter(Mandatory = $false)] + [string[]]$Append, + [Parameter(Mandatory = $false)] + [string[]]$Prepend, + [Parameter(Mandatory = $false)] + [string[]]$Remove, + [string]$PathSeparator = [IO.Path]::PathSeparator + ) + + $processPath = @() + $userPath = @() + $machinePath = @() + $processPathAny = $false + $userPathAny = $false + $machinePathAny = $false + $processPathDirty = $false + $userPathDirty = $false + $machinePathDirty = $false + + $modifyAny = $StoreName -eq 'Any' + + if (Any $Remove) { + $Remove = $Remove.Where({![string]::IsNullOrWhiteSpace($PSItem)}) + } + + if (Any $Prepend) { + $Prepend = $Prepend.Where({![string]::IsNullOrWhiteSpace($PSItem)}) + } + + if (Any $Append) { + $Append = $Append.Where({![string]::IsNullOrWhiteSpace($PSItem)}) + } + + if (!(Any $Remove)) { + $Remove = @() + } + + $count = 0 + + # if we are prepending or appending any paths, yoink them from their existing place + if (Any $Prepend) { + $count = $Prepend.Count + foreach ($prependPath in $Prepend) { + if ($Remove -notcontains $prependPath) { + $Remove += $prependPath + } + } + } + + if (Any $Append) { + # Can't both prepend and append entries. + # Assume prepend wins + for ($i=0;$i -lt $count; $i++) { + $removeIfFound = $Prepend[$i] + $at = $Append.IndexOf($removeIfFound) + if ($at -gt -1) { + $Append.RemoveAt($at) + } + } + foreach ($appendPath in $Append) { + if ($Remove -notcontains $appendPath) { + $Remove += $appendPath + } + } + } + + if (!(Any $Remove)) { + Write-Warning "No paths provided to modify. Exiting with no action taken." + return + } + + if (('Process','Any') -contains $StoreName) { + $processPath = (Get-EnvironmentVariable -Name Path -StoreName 'Process') -split $PathSeparator + $processPathAny = (Any $processPath) + + # If we were told to modify the process variable but none exists, we should still modify it + if (!$processPathAny -and !$modifyAny) { + $processPath = @() + $processPathAny = $true + } + + if ($processPathAny) { + # coerce so we can RemoveAt + [System.Collections.ArrayList]$processPath = [System.Collections.ArrayList]$processPath + } + } + + if (('User','Any') -contains $StoreName) { + $userPath = (Get-EnvironmentVariable -Name Path -StoreName 'User') -split $PathSeparator + $userPathAny = (Any $userPath) + + # If we were told to modify the user variable but none exists, we should still modify it + if (!$userPathAny -and !$modifyAny) { + $userPath = @() + $userPathAny = $true + } + + if ($userPathAny) { + # coerce so we can RemoveAt + [System.Collections.ArrayList]$userPath = [System.Collections.ArrayList]$userPath + } + } + + if (('Machine','Any') -contains $StoreName) { + $machinePath = (Get-EnvironmentVariable -Name Path -StoreName 'Machine') -split $PathSeparator + $machinePathAny = (Any $machinePath) + + # If we were told to modify the machine variable but none exists, we should still modify it + if (!$machinePathAny -and !$modifyAny) { + $machinePath = @() + $machinePathAny = $true + } + + if ($machinePathAny) { + # coerce so we can RemoveAt + [System.Collections.ArrayList]$machinePath = [System.Collections.ArrayList]$machinePath + } + } + + $count = $Remove.Count + + # remove first + if (Any $Remove) { + if ($processPathAny) { + for ($i=0;$i -lt $count; $i++) { + $removeIfFound = $Remove[$i] + $at = $processPath.IndexOf($removeIfFound) + if ($at -gt -1) { + $processPath.RemoveAt($at) + $processPathDirty = $true + } + } + } + if ($userPathAny) { + for ($i=0;$i -lt $count; $i++) { + $removeIfFound = $Remove[$i] + $at = $userPath.IndexOf($removeIfFound) + if ($at -gt -1) { + $userPath.RemoveAt($at) + $userPathDirty = $true + } + } + } + if ($machinePathAny) { + for ($i=0;$i -lt $count; $i++) { + $removeIfFound = $Remove[$i] + $at = $machinePath.IndexOf($removeIfFound) + if ($at -gt -1) { + $machinePath.RemoveAt($at) + $machinePathDirty = $true + } + } + } + } + + # then append + if (Any $Append) { + if ($processPathAny) { + foreach($appendPath in $Append) { + if ($processPath -notcontains $appendPath) { + $processPath.Add($appendPath) + } + } + $processPathDirty = $true + } + if ($userPathAny) { + foreach($appendPath in $Append) { + if ($userPath -notcontains $appendPath) { + $userPath.Add($appendPath) + } + } + $userPathDirty = $true + } + if ($machinePathAny) { + foreach($appendPath in $Append) { + if ($machinePath -notcontains $appendPath) { + $machinePath.Add($appendPath) + } + } + $machinePathDirty = $true + } + } + + # then prepend + if (Any $Prepend) { + if ($processPathAny) { + $processPath = $Prepend + $processPath + $processPathDirty = $true + } + if ($userPathAny) { + $userPath = $Prepend + $userPath + $userPathDirty = $true + } + if ($machinePathAny) { + $machinePath = $Prepend + $machinePath + $machinePathDirty = $true + } + } + + # ensure we don't write bad paths by checking for .Where({![string]::IsNullOrWhiteSpace($PSItem)}) + + if ($processPathDirty) { + Set-EnvironmentVariable -Name Path -Value ($processPath.Where({![string]::IsNullOrWhiteSpace($PSItem)}) -join $PathSeparator) -StoreName 'Process' + } + + if ($userPathDirty) { + Set-EnvironmentVariable -Name Path -Value ($userPath.Where({![string]::IsNullOrWhiteSpace($PSItem)}) -join $PathSeparator) -StoreName 'User' + } + + if ($machinePathDirty) { + Set-EnvironmentVariable -Name Path -Value ($machinePath.Where({![string]::IsNullOrWhiteSpace($PSItem)}) -join $PathSeparator) -StoreName 'Machine' + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Machine.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Machine.pester.ps1 new file mode 100644 index 0000000..913d427 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Machine.pester.ps1 @@ -0,0 +1,111 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "Process" { + Mock -CommandName Get-EnvironmentVariable -MockWith { throw 'Asked for non-Process paths' } + Mock -CommandName Set-EnvironmentVariable -MockWith { throw 'Should not set non-Process paths' } + + Context "Append 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + $Value | Should -Be "path2;path3" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Machine' -Append 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Prepend 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + $Value | Should -Be "path3;path2" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Machine' -Prepend 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Append 5 Remove 5 with strings that don't exist in the first path" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path10;path11;path12;path13;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Machine' -Append 'path10','path11','path12','path13','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Append 3 Remove 5 with a weird collection" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "path14;path13;path10;path11;path12;path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path13;path12;path10;path11;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Machine' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context 'It is gonna throw!' { + It 'Throws for User!' { + { Set-PathVariable -StoreName 'User' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + + It 'Throws for Process!' { + { Set-PathVariable -StoreName 'Process' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Mixed.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Mixed.pester.ps1 new file mode 100644 index 0000000..ff789f7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Mixed.pester.ps1 @@ -0,0 +1,106 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "Process" { + Mock -CommandName Get-EnvironmentVariable -MockWith { throw 'Asked for non-Process paths' } + Mock -CommandName Set-EnvironmentVariable -MockWith { throw 'Should not set non-Process paths' } + + # This simulates a simple real-world experience + Context "Prepend terraform path / move it around" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin;" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + $Value | Should -Be "C:\ProgramData\terraform;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + # Note that this loses the trailing ; which doesn't show up well in massively sidescrolling situations + $Value | Should -Be "C:\ProgramData\terraform;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + $Value | Should -Be "C:\ProgramData\terraform;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Any' -Prepend 'C:\ProgramData\terraform' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + # This simulates a simple real-world experience with bad params + Context "Prepend terraform path / move it around - but with bad params" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin;" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + $Value | Should -Be "C:\ProgramData\terraform;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + # Note that this loses the trailing ; which doesn't show up well in massively sidescrolling situations + $Value | Should -Be "C:\ProgramData\terraform;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + $Value | Should -Be "C:\ProgramData\terraform;C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd" + } + + It 'Does not throw' { + # Listen, sometimes we make bad param decisions. It happens. + { Set-PathVariable -StoreName 'Any' -Prepend 'C:\ProgramData\terraform',"" -Remove @($null) -Append "","" } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + Context "Does nothing if there is nothing to do" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files (x86)\LinqPad5;C:\Users\cbrand\AppData\Roaming\npm;C:\Users\cbrand\AppData\Local\Programs\Microsoft VS Code\bin;C:\Users\cbrand\.dotnet\tools;C:\Users\cbrand\AppData\Local\Microsoft\WindowsApps;C:\Program Files\WindowsPowerShell\Modules\Pester\4.10.1\bin;" } + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { return "C:\Program Files (x86)\Parallels\Parallels Tools\Applications;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Airtame;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\ProgramData\chocolatey\bin;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\nodejs\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Amazon\AWSCLI\;C:\Program Files\Microsoft\Web Platform Installer\;C:\Program Files\dotnet\;C:\Program Files (x86)\dotnet\;C:\Program Files\Amazon\AWSSAMCLI\bin\;C:\Program Files\Docker\Docker\resources\bin;C:\ProgramData\DockerDesktop\version-bin;C:\Program Files (x86)\GitExtensions\;C:\Program Files (x86)\dotnet-core-uninstall\;C:\Program Files\Git\cmd" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + throw 'This should not get called' + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + throw 'This should not get called' + } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -MockWith { + throw 'This should not get called' + } + + It 'Does not throw' { + # Listen, sometimes we make bad param decisions. It happens. + { Set-PathVariable -StoreName 'Any' -Prepend "" -Remove @($null) -Append "","" } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Process.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Process.pester.ps1 new file mode 100644 index 0000000..c85987f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/Process.pester.ps1 @@ -0,0 +1,111 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "Process" { + Mock -CommandName Get-EnvironmentVariable -MockWith { throw 'Asked for non-Process paths' } + Mock -CommandName Set-EnvironmentVariable -MockWith { throw 'Should not set non-Process paths' } + + Context "Append 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + $Value | Should -Be "path2;path3" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Process' -Append 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + Context "Prepend 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + $Value | Should -Be "path3;path2" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Process' -Prepend 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + Context "Append 5 Remove 5 with strings that don't exist in the first path" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path10;path11;path12;path13;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Process' -Append 'path10','path11','path12','path13','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + Context "Append 3 Remove 5 with a weird collection" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { return "path14;path13;path10;path11;path12;path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path13;path12;path10;path11;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'Process' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 1 + } + } + + Context 'It is gonna throw!' { + It 'Throws for User!' { + { Set-PathVariable -StoreName 'User' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + + It 'Throws for Machine!' { + { Set-PathVariable -StoreName 'Machine' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/User.pester.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/User.pester.ps1 new file mode 100644 index 0000000..5823828 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-PathVariable/User.pester.ps1 @@ -0,0 +1,111 @@ +$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +. "$here.ps1" + +Describe "Process" { + Mock -CommandName Get-EnvironmentVariable -MockWith { throw 'Asked for non-Process paths' } + Mock -CommandName Set-EnvironmentVariable -MockWith { throw 'Should not set non-Process paths' } + + Context "Append 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + $Value | Should -Be "path2;path3" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'User' -Append 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Prepend 1 Remove 1" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + $Value | Should -Be "path3;path2" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'User' -Prepend 'path3' -Remove 'path1' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Append 5 Remove 5 with strings that don't exist in the first path" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path10;path11;path12;path13;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'User' -Append 'path10','path11','path12','path13','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context "Append 3 Remove 5 with a weird collection" { + Mock -CommandName Get-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { return "path14;path13;path10;path11;path12;path1;path2" } + Mock -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -MockWith { + # remove then add, so any adds should be there even if the remove says to remove them + $Value | Should -Be "path13;path12;path10;path11;path14" + } + + It 'Does not throw' { + { Set-PathVariable -StoreName 'User' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Not -Throw + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'User' } -Times 1 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Machine' } -Times 0 + } + + It 'Called the Set-EnvironmentVariable correctly' { + Assert-MockCalled -CommandName Set-EnvironmentVariable -ParameterFilter { $Name -eq 'Path' -and $StoreName -eq 'Process' } -Times 0 + } + } + + Context 'It is gonna throw!' { + It 'Throws for Process!' { + { Set-PathVariable -StoreName 'Process' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + + It 'Throws for Machine!' { + { Set-PathVariable -StoreName 'Machine' -Append 'path10','path11','path14' -Remove 'path1','path2','path10','path11','path14' } | Should -Throw + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-RepoCheckpointPath.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-RepoCheckpointPath.ps1 new file mode 100644 index 0000000..7f3c871 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-RepoCheckpointPath.ps1 @@ -0,0 +1,16 @@ +function Set-RepoCheckpointPath { +<# +.SYNOPSIS + Set the path used by the Checkpoint-AllAvailableRepos and other places + +.PARAMETER Path + This path does not need to exist, it will get created if used. +#> + [CmdletBinding()] + [OutputType([string])] + param( + $Path + ) + + Set-EnvironmentVariable -Name "Checkpoint_DefaultPath" -Value $Path -StoreName User +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-TeamCityParameter.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-TeamCityParameter.ps1 new file mode 100644 index 0000000..242c06c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-TeamCityParameter.ps1 @@ -0,0 +1,54 @@ +function Set-TeamCityParameter { +<# +.SYNOPSIS + Set a value on the TeamCity parameter for the job. + +.DESCRIPTION + The value set here will not be available to other processes until the build-step completes. + You can use this to set %dep.*.parameter% style parameters on the build server. + +.PARAMETER Name + The name of the parameter to set. + This value can start with env: or system: to affect those specific parameter sets. + This value will get sanitized. + This should not affect your end-value, but special characters may need to be validated on first-usage. + +.PARAMETER Value + The value to be set. + This value will get sanitized. + This should not affect your end-value, but special characters may need to be validated on first-usage. + +.EXAMPLE + Set-TeamCityParameter -Name 'rev.dep.*.SomeKey' -Value 'Hi mom!' +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Value + ) + + $logLead = Get-LogLeadName + + $sanitizedName = ConvertTo-SafeTeamCityMessage -InputText $Name + $sanitizedValue = ConvertTo-SafeTeamCityMessage -InputText $Value + + if ($sanitizedName.Substring(0, 6) -eq 'system:') { + Write-Host "$logLead : Setting a TeamCity parameter on SYSTEM with Name [$($sanitizedName.Substring(7))]" + } + + if ($sanitizedName.Substring(0, 6) -eq 'env:') { + Write-Host "$logLead : Setting a TeamCity parameter on ENVIRONMENT with Name [$($sanitizedName.Substring(4))]" + } + + if (Test-IsTeamCityProcess) { + Write-Host "##teamcity[setParameter name='$sanitizedName' value='$sanitizedValue']" + } else { + Write-Host "$logLead : Would set a parameter on the build to Name: [$sanitizedName] Value: [$sanitizedValue]" + # TODO: Do we want to use this to set local variables? + # We could have ENV: or SYSTEM: set Env-Vars in Process/Machine, and others Set-Variable -Scope Script/Global + # Not super-useful, maybe, but could be. Worth a thought. + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Set-WindowsDisplayPercentage.ps1 b/Modules/Cole.PowerShell.Developer/Public/Set-WindowsDisplayPercentage.ps1 new file mode 100644 index 0000000..e942d2c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Set-WindowsDisplayPercentage.ps1 @@ -0,0 +1,27 @@ +function Set-WindowsDisplayPercentage { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet(100,125,150,175,200)] + [Alias("Percent")] + $Scaling + ) + # $scaling = 0 : 100% (default) + # $scaling = 1 : 125% + # $scaling = 2 : 150% + # $scaling = 3 : 175% + $apiValue = switch($Scaling) { + 100 { 0 } + 125 { 1 } + 150 { 2 } + 175 { 3 } + 200 { 4 } + } + + $source = @" +[DllImport("user32.dll", EntryPoint = "SystemParametersInfo")] +public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, uint pvParam, uint fWinIni); +"@ + $apicall = Add-Type -MemberDefinition $source -Name WinAPICall -Namespace SystemParamInfo -PassThru + $apicall::SystemParametersInfo(0x009F, $apiValue, $null, 1) | Out-Null +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-Box.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-Box.ps1 new file mode 100644 index 0000000..839b530 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-Box.ps1 @@ -0,0 +1,720 @@ +function Show-Box { +<# +.SYNOPSIS + Creates a box on screen using similar syntax to CSS + Margin and Padding can be input as in CSS with values in [Top Right Bottom Left] form, where the values for top and bottom are whole numbers only, and for left and right can be percents or integers + Using the more specific values of MarginTop, PaddingLeft, etc, will override the generic Margin or Padding properties specifiers. + + When Border is specified, it will apply to all non-overridden values as appropriate. + + Specifity overrides generic. +#> + param ( + [Parameter(Mandatory = $false)] + [string[]]$TextContent, + [Parameter(Mandatory = $false)] + [string[]]$TitleText, + [Parameter(Mandatory = $false)] + [string]$Width = '100%', + [Parameter(Mandatory = $false)] + [string]$Padding = 1, + [Parameter(Mandatory = $false)] + [string]$Margin = 0, + [Parameter(Mandatory = $false)] + [string]$MarginLeft = $null, + [Parameter(Mandatory = $false)] + [string]$MarginRight = $null, + [Parameter(Mandatory = $false)] + [string]$MarginTop = $null, + [Parameter(Mandatory = $false)] + [string]$MarginBottom = $null, + [Parameter(Mandatory = $false)] + [string]$PaddingLeft = $null, + [Parameter(Mandatory = $false)] + [string]$PaddingRight = $null, + [Parameter(Mandatory = $false)] + [string]$PaddingTop = $null, + [Parameter(Mandatory = $false)] + [string]$PaddingBottom = $null, + [Parameter(Mandatory = $false)] + [ValidateSet('Thin','Thick','Line','Bold','Dotted','Dashed','DottedBold','BoldDashed','Wide','ExtraWide','Double','WideBold','ExtraWideBold','Wavy','None')] + [string]$Border = 'Line', + [Parameter(Mandatory = $false)] + [ValidateSet('Thin','Thick','Line','Bold','Dotted','Dashed','DottedBold','BoldDashed','Wide','ExtraWide','Double','WideBold','ExtraWideBold','Wavy','None')] + [string]$BorderTop, + [Parameter(Mandatory = $false)] + [ValidateSet('Thin','Thick','Line','Bold','Dotted','Dashed','DottedBold','BoldDashed','Wide','ExtraWide','Double','WideBold','ExtraWideBold','Wavy','None')] + [string]$BorderLeft, + [Parameter(Mandatory = $false)] + [ValidateSet('Thin','Thick','Line','Bold','Dotted','Dashed','DottedBold','BoldDashed','Wide','ExtraWide','Double','WideBold','ExtraWideBold','Wavy','None')] + [string]$BorderRight, + [Parameter(Mandatory = $false)] + [ValidateSet('Thin','Thick','Line','Bold','Dotted','Dashed','DottedBold','BoldDashed','Wide','ExtraWide','Double','WideBold','ExtraWideBold','Wavy','None')] + [string]$BorderBottom, + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BorderColor = 'Default', + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BorderColorTop, + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BorderColorLeft, + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BorderColorRight, + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BorderColorBottom, + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$TextColor = 'Default', + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$BackgroundColor = 'Default', + [switch]$UseCornerRadius, + [switch]$Center, + [Parameter(Mandatory = $false)] + [ValidateSet('Left','Center','Right','Justify')] + [string]$TextAlignment = 'Left', + [switch]$TextTruncate + ) + + $logLead = Get-LogLeadName + $isNumberRegex = "^\d+$" + $isPercentRegex = "^\d+%$" + $screenWidth = Get-ConsoleDisplayWidth + $borderWidthCount = 0 + $ResetColor = $PSStyle.Reset + + if ($Center) { + # Center flag overrides textAlignment + $TextAlignment = 'Center' + } + + if ($TextColor -eq 'Default') { + $_textColor = $ResetColor + } else { + $_textColor = $PSStyle.ForegroundColor.$TextColor + } + + if ($BackgroundColor -eq 'Default') { + $_backgroundColor = $ResetColor + } else { + $_backgroundColor = $PSStyle.BackgroundColor.$BackgroundColor + } + +#region calculate border colors + if ([string]::IsNullOrWhiteSpace($BorderColorTop)) { + $BorderColorTop = $BorderColor + } + + if ([string]::IsNullOrWhiteSpace($BorderColorLeft)) { + $BorderColorLeft = $BorderColor + } + + if ([string]::IsNullOrWhiteSpace($BorderColorRight)) { + $BorderColorRight = $BorderColor + } + + if ([string]::IsNullOrWhiteSpace($BorderColorBottom)) { + $BorderColorBottom = $BorderColor + } + + if ($BorderColorTop -eq 'Default') { + $_borderColorTop = $ResetColor + } else { + $_borderColorTop = $PSStyle.ForegroundColor.$BorderColorTop + } + if ($BorderColorLeft -eq 'Default') { + $_borderColorLeft = $ResetColor + } else { + $_borderColorLeft = $PSStyle.ForegroundColor.$BorderColorLeft + } + if ($BorderColorRight -eq 'Default') { + $_borderColorRight = $ResetColor + } else { + $_borderColorRight = $PSStyle.ForegroundColor.$BorderColorRight + } + if ($BorderColorBottom -eq 'Default') { + $_borderColorBottom = $ResetColor + } else { + $_borderColorBottom = $PSStyle.ForegroundColor.$BorderColorBottom + } + + +#endregion calculate border colors + +#region calculate borders + if ([string]::IsNullOrWhiteSpace($BorderTop)) { + $BorderTop = $Border + } + if ([string]::IsNullOrWhiteSpace($BorderLeft)) { + $BorderLeft = $Border + } + if ([string]::IsNullOrWhiteSpace($BorderRight)) { + $BorderRight = $Border + } + if ([string]::IsNullOrWhiteSpace($BorderBottom)) { + $BorderBottom = $Border + } + + $boldLeft = $BorderLeft.EndsWith('Bold') + $boldRight = $BorderRight.EndsWith('Bold') + $boldTop = $BorderTop.EndsWith('Bold') + $boldBottom = $BorderBottom.EndsWith('Bold') + $doubleLeft = $BorderLeft -eq 'Double' + $doubleRight = $BorderRight -eq 'Double' + $doubleTop = $BorderTop -eq 'Double' + $doubleBottom = $BorderBottom -eq 'Double' + $lineLeft = !$boldLeft -and !$doubleLeft + $lineRight = !$boldRight -and !$doubleRight + $lineTop = !$boldTop -and !$doubleTop + $lineBottom = !$boldBottom -and !$doubleBottom + $noneLeft = $BorderLeft -eq 'None' + $noneRight = $BorderRight -eq 'None' + $noneTop = $BorderTop -eq 'None' + $noneBottom = $BorderBottom -eq 'None' + + $leftBottomCorner = "" + $leftTopCorner = "" + $rightTopCorner = "" + $rightBottomCorner = "" + + if ($noneLeft -and $noneBottom) { + $leftBottomCorner = "" + } elseif ($lineLeft -and $boldBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.BoldHorizontal + } elseif ($boldLeft -and $lineBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.BoldVertical + } elseif ($boldLeft -and $boldBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.Bold + } elseif ($lineLeft -and $doubleBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.DoubleHorizontal + } elseif ($doubleLeft -and $lineBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.DoubleVertical + } elseif ($doubleLeft -and $doubleBottom) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.Double + } else { + $leftBottomCorner = $PSBox.Corner.BottomLeft.Line + } + + if ($noneRight -and $noneBottom) { + $rightBottomCorner = "" + } elseif ($lineRight -and $boldBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.BoldHorizontal + } elseif ($boldRight -and $lineBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.BoldVertical + } elseif ($boldRight -and $boldBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.Bold + } elseif ($lineRight -and $doubleBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.DoubleHorizontal + } elseif ($doubleRight -and $lineBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.DoubleVertical + } elseif ($doubleRight -and $doubleBottom) { + $rightBottomCorner = $PSBox.Corner.BottomRight.Double + } else { + $rightBottomCorner = $PSBox.Corner.BottomRight.Line + } + + if ($noneLeft -and $noneTop) { + $leftTopCorner = "" + } elseif ($lineLeft -and $boldTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.BoldHorizontal + } elseif ($boldLeft -and $lineTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.BoldVertical + } elseif ($boldLeft -and $boldTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.Bold + } elseif ($lineLeft -and $doubleTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.DoubleHorizontal + } elseif ($doubleLeft -and $lineTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.DoubleVertical + } elseif ($doubleLeft -and $doubleTop) { + $leftTopCorner = $PSBox.Corner.TopLeft.Double + } else { + $leftTopCorner = $PSBox.Corner.TopLeft.Line + } + + if ($noneRight -and $noneTop) { + $rightTopCorner = "" + } elseif ($lineRight -and $boldTop) { + $rightTopCorner = $PSBox.Corner.TopRight.BoldHorizontal + } elseif ($boldRight -and $lineTop) { + $rightTopCorner = $PSBox.Corner.TopRight.BoldVertical + } elseif ($boldRight -and $boldTop) { + $rightTopCorner = $PSBox.Corner.TopRight.Bold + } elseif ($lineRight -and $doubleTop) { + $rightTopCorner = $PSBox.Corner.TopRight.DoubleHorizontal + } elseif ($doubleRight -and $lineTop) { + $rightTopCorner = $PSBox.Corner.TopRight.DoubleVertical + } elseif ($doubleRight -and $doubleTop) { + $rightTopCorner = $PSBox.Corner.TopRight.Double + } else { + # always curve down + $rightTopCorner = $PSBox.Corner.TopRight.Line + } + + # TODO: Make the adjoining lines always transition from thin to thick where possible + if ($UseCornerRadius) { + $leftBottomCorner = $PSBox.Corner.BottomLeft.Round + $leftTopCorner = $PSBox.Corner.TopLeft.Round + $rightTopCorner = $PSBox.Corner.TopRight.Round + $rightBottomCorner = $PSBox.Corner.BottomRight.Round + } + + $topBorderCharacters = switch ($BorderTop) { + 'Thin' { $PSBox.Horizontal.Line } + 'Line' { $PSBox.Horizontal.Line } + 'Thick' { $PSBox.Horizontal.Bold } + 'Bold' { $PSBox.Horizontal.Bold } + 'Dotted' { $PSBox.Horizontal.DashedNarrow } + 'Dashed' { $PSBox.Horizontal.Dashed } + 'DottedBold' { $PSBox.Horizontal.BoldDashedNarrow } + 'BoldDashed' { $PSBox.Horizontal.BoldDashed } + 'Wide' { $PSBox.Horizontal.DashedWide } + 'ExtraWide' { $PSBox.Horizontal.LeftHalf,$PSBox.Horizontal.RightHalf } + 'Double' { $PSBox.Horizontal.Double } + 'WideBold' { $PSBox.Horizontal.BoldDashedWide } + 'ExtraWideBold' { $PSBox.Horizontal.BoldLeftHalf,$PSBox.Horizontal.BoldRightHalf } + 'Wavy' { $PSBox.Horizontal.BoldRightThinLeft } + 'None' { "" } + } + + $bottomBorderCharacters = switch ($BorderBottom) { + 'Thin' { $PSBox.Horizontal.Line } + 'Line' { $PSBox.Horizontal.Line } + 'Thick' { $PSBox.Horizontal.Bold } + 'Bold' { $PSBox.Horizontal.Bold } + 'Dotted' { $PSBox.Horizontal.DashedNarrow } + 'Dashed' { $PSBox.Horizontal.Dashed } + 'DottedBold' { $PSBox.Horizontal.BoldDashedNarrow } + 'BoldDashed' { $PSBox.Horizontal.BoldDashed } + 'Wide' { $PSBox.Horizontal.DashedWide } + 'ExtraWide' { $PSBox.Horizontal.LeftHalf,$PSBox.Horizontal.RightHalf } + 'Double' { $PSBox.Horizontal.Double } + 'WideBold' { $PSBox.Horizontal.BoldDashedWide } + 'ExtraWideBold' { $PSBox.Horizontal.BoldLeftHalf,$PSBox.Horizontal.BoldRightHalf } + 'Wavy' { $PSBox.Horizontal.BoldRightThinLeft } + 'None' { "" } + } + + $leftBorderCharacters = switch ($BorderLeft) { + 'Thin' { $PSBox.Vertical.Line } + 'Line' { $PSBox.Vertical.Line } + 'Thick' { $PSBox.Vertical.Bold } + 'Bold' { $PSBox.Vertical.Bold } + 'Dotted' { $PSBox.Vertical.DashedNarrow } + 'Dashed' { $PSBox.Vertical.Dashed } + 'DottedBold' { $PSBox.Vertical.BoldDashedNarrow } + 'BoldDashed' { $PSBox.Vertical.BoldDashed } + 'Wide' { $PSBox.Vertical.DashedWide } + 'ExtraWide' { $PSBox.Vertical.DashedWide } + 'Double' { $PSBox.Vertical.Double } + 'WideBold' { $PSBox.Vertical.BoldDashedWide } + 'ExtraWideBold' { $PSBox.Vertical.Bold } + 'Wavy' { $PSBox.Vertical.BoldBottomThinTop } + 'None' { "" } + } + + $rightBorderCharacters = switch ($BorderRight) { + 'Thin' { $PSBox.Vertical.Line } + 'Line' { $PSBox.Vertical.Line } + 'Thick' { $PSBox.Vertical.Bold } + 'Bold' { $PSBox.Vertical.Bold } + 'Dotted' { $PSBox.Vertical.DashedNarrow } + 'Dashed' { $PSBox.Vertical.Dashed } + 'DottedBold' { $PSBox.Vertical.BoldDashedNarrow } + 'BoldDashed' { $PSBox.Vertical.BoldDashed } + 'Wide' { $PSBox.Vertical.DashedWide } + 'ExtraWide' { $PSBox.Vertical.DashedWide } + 'Double' { $PSBox.Vertical.Double } + 'WideBold' { $PSBox.Vertical.BoldDashedWide } + 'ExtraWideBold' { $PSBox.Vertical.Bold } + 'Wavy' { $PSBox.Vertical.BoldBottomThinTop } + 'None' { "" } + } + + if (![string]::IsNullOrWhiteSpace($leftBorderCharacters)) { + $borderWidthCount += 1 + } + + if (![string]::IsNullOrWhiteSpace($rightBorderCharacters)) { + $borderWidthCount += 1 + } + $screenWidth = $screenWidth - $borderWidthCount # account for the vertical bars themselves +#endregion calculate borders + +#region calculate margins + $Margin = "$Margin".Trim() + $marginIsNumber = $Margin -match $isNumberRegex + $marginSplits = ($Margin -split '\s+') + $marginHasFourParts = $marginSplits.Count -eq 4 + $marginHasTwoParts = $marginSplits.Count -eq 2 + if ($marginSplits.Count -gt 1) { + if ($marginHasFourParts) { + $_marginTop = $marginSplits[0] + $_marginRight = $marginSplits[1] + $_marginBottom = $marginSplits[2] + $_marginLeft = $marginSplits[3] + } elseif ($marginHasTwoParts) { + $_marginTop = $marginSplits[0] + $_marginRight = $marginSplits[1] + $_marginBottom = $marginSplits[0] + $_marginLeft = $marginSplits[1] + } else { + throw "$logLead : Margin should be specified in four parts, representing in-order [Top Right Bottom Left], or in two parts [vertical horizontal]" + } + } else { + # There's only one value, so we can just assign it to the internals before it gets overwritten + $_marginTop = $Margin + $_marginBottom = $Margin + $_marginRight = $Margin + $_marginLeft = $Margin + } + + if (![string]::IsNullOrWhiteSpace("$MarginTop")) { + $_marginTop = $MarginTop + } + if (![string]::IsNullOrWhiteSpace("$MarginRight")) { + $_marginRight = $MarginRight + } + if (![string]::IsNullOrWhiteSpace("$MarginBottom")) { + Write-Host "setting margin bottom to $MarginBottom" + $_marginBottom = $MarginBottom + } + if (![string]::IsNullOrWhiteSpace("$MarginLeft")) { + $_marginLeft = $MarginLeft + } +#region calculate top margin + if ($null -ne $_marginTop) { + if ($_marginTop -notmatch $isNumberRegex) { + throw "$logLead : Margin Top should be a whole number, not a percent or non-numeric value" + } else { + $_marginTop = [int]$_marginTop + } + } else { + $_marginTop = 0 + } +#endregion calculate top margin +#region calculate right margin + if ($null -ne $_marginRight) { + $_marginRightIsNumber = $_marginRight -match $isNumberRegex + $_marginRightIsPercent = $_marginRight -match $isPercentRegex + if (!$_marginRightIsNumber -and !$_marginRightIsPercent) { + throw "$logLead : Right margin should be specified as a percent or a whole number" + } + + if ($_marginRightIsNumber) { + $_marginRight = [int]$_marginRight + } + if ($_marginRightIsPercent) { + $_marginRight = [System.Math]::Round((([int]($_marginRight.Substring(0,$_marginRight.Length - 1)) * $screenWidth )/ 100.0)) + } + } else { + $_marginRight = 0 + } +#endregion calculate right margin +#region calculate bottom margin + if ($null -ne $_marginBottom) { + if ($_marginBottom -notmatch $isNumberRegex) { + throw "$logLead : Margin bottom should be a whole number, not a percent or non-numeric value" + } else { + $_marginBottom = [int]$_marginBottom + } + } else { + $_marginBottom = 0 + } +#endregion calculate bottom margin +#region calculate left margin + if ($null -ne $_marginLeft) { + $_marginLeftIsNumber = $_marginLeft -match $isNumberRegex + $_marginLeftIsPercent = $_marginLeft -match $isPercentRegex + if (!$_marginLeftIsNumber -and !$_marginLeftIsPercent) { + throw "$logLead : Left margin should be specified as a percent or a whole number" + } + + if ($_marginLeftIsNumber) { + $_marginLeft = [int]$_marginLeft + } + if ($_marginLeftIsPercent) { + $_marginLeft = [System.Math]::Round((([int]($_marginLeft.Substring(0,$_marginLeft.Length - 1)) * $screenWidth )/ 100.0)) + } + } else { + $_marginLeft = 0 + } +#endregion calculate left margin +#endregion calculate margins + +#region calculate paddings + $Padding = "$Padding".Trim() + $paddingIsNumber = $Padding -match $isNumberRegex + $paddingSplits = ($Padding -split '\s+') + $paddingHasFourParts = $paddingSplits.Count -eq 4 + $paddingHasTwoParts = $paddingSplits.Count -eq 2 + if ($paddingSplits.Count -gt 1) { + if ($paddingHasFourParts) { + $_paddingTop = $paddingSplits[0] + $_paddingRight = $paddingSplits[1] + $_paddingBottom = $paddingSplits[2] + $_paddingLeft = $paddingSplits[3] + } elseif ($paddingHasTwoParts) { + $_paddingTop = $paddingSplits[0] + $_paddingRight = $paddingSplits[1] + $_paddingBottom = $paddingSplits[0] + $_paddingLeft = $paddingSplits[1] + } else { + throw "$logLead : Padding should be specified in four parts, representing in-order [Top Right Bottom Left], or in two parts [vertical horizontal]" + } + } else { + # There's only one value, so we can just assign it to the internals before it gets overwritten + $_paddingTop = $Padding + $_paddingBottom = $Padding + $_paddingRight = $Padding + $_paddingLeft = $Padding + } + + if (![string]::IsNullOrWhiteSpace("$PaddingTop")) { + $_paddingTop = $PaddingTop + } + if (![string]::IsNullOrWhiteSpace("$PaddingRight")) { + $_paddingRight = $PaddingRight + } + if (![string]::IsNullOrWhiteSpace("$PaddingBottom")) { + $_paddingBottom = $PaddingBottom + } + if (![string]::IsNullOrWhiteSpace("$PaddingLeft")) { + $_paddingLeft = $PaddingLeft + } + +#region calculate box width + $boxWidth = $screenWidth - $_marginLeft - $_marginRight + + $widthIsNumber = $Width -match $isNumberRegex + $widthIsPercent = $Width -match $isPercentRegex + if ($widthIsNumber) { + $boxWidth = [int]$Width + if ($boxWidth -gt $screenWidth) { + $boxWidth = $screenWidth + } + } + if ($widthIsPercent) { + $boxWidth = [System.Math]::Round((([int]($Width.Substring(0,$Width.Length - 1)) * $boxWidth )/ 100.0)) + } + if (!$widthIsNumber -and !$widthIsPercent) { + throw "$logLead : Width parameter must be an int or a percent expressed as an integer and percent sign. Examples: 10, 25%, 100" + } +#endregion calculate box width + +#region calculate top padding + if ($null -ne $_paddingTop) { + if ($_paddingTop -notmatch $isNumberRegex) { + throw "$logLead : Padding Top should be a whole number, not a percent or non-numeric value" + } else { + $_paddingTop = [int]$_paddingTop + } + } else { + $_paddingTop = 0 + } +#endregion calculate top padding +#region calculate right padding + if ($null -ne $_paddingRight) { + $_paddingRightIsNumber = $_paddingRight -match $isNumberRegex + $_paddingRightIsPercent = $_paddingRight -match $isPercentRegex + if (!$_paddingRightIsNumber -and !$_paddingRightIsPercent) { + throw "$logLead : Right padding should be specified as a percent or a whole number" + } + + if ($_paddingRightIsNumber) { + $_paddingRight = [int]$_paddingRight + } + if ($_paddingRightIsPercent) { + $_paddingRight = [System.Math]::Round((([int]($_paddingRight.Substring(0,$_paddingRight.Length - 1)) * $boxWidth )/ 100.0)) + } + } else { + $_paddingRight = 0 + } +#endregion calculate right padding +#region calculate bottom padding + if ($null -ne $_paddingBottom) { + if ($_paddingBottom -notmatch $isNumberRegex) { + throw "$logLead : Padding bottom should be a whole number, not a percent or non-numeric value" + } else { + $_paddingBottom = [int]$_paddingBottom + } + } else { + $_paddingBottom = 0 + } +#endregion calculate bottom padding +#region calculate left padding + if ($null -ne $_paddingLeft) { + $_paddingLeftIsNumber = $_paddingLeft -match $isNumberRegex + $_paddingLeftIsPercent = $_paddingLeft -match $isPercentRegex + if (!$_paddingLeftIsNumber -and !$_paddingLeftIsPercent) { + throw "$logLead : Left padding should be specified as a percent or a whole number" + } + + if ($_paddingLeftIsNumber) { + $_paddingLeft = [int]$_paddingLeft + } + if ($_paddingLeftIsPercent) { + $_paddingLeft = [System.Math]::Round((([int]($_paddingLeft.Substring(0,$_paddingLeft.Length - 1)) * $boxWidth )/ 100.0)) + } + } else { + $_paddingLeft = 0 + } +#endregion calculate left padding +#endregion calculate paddings + +#region calculate content width + $TextContentWidth = $boxWidth - $_paddingLeft - $_paddingRight + if ($TextContentWidth -lt 5) { + throw "$logLead : The calculated contentWidth is too narrow. Make your width larger, or your padding smaller" + } +#endregion calculate content width + + $leftMargin = "".PadRight($_marginLeft) + $leftPadding = "".PadRight($_paddingLeft) + $rightMargin = "".PadRight($_marginRight) + $rightPadding = "".PadRight($_paddingRight) + + $output = @() + $paddingLine = "".PadRight($TextContentWidth, ' ') + $topLine = ($topBorderCharacters * $boxWidth) + $bottomLine = ($bottomBorderCharacters * $boxWidth) + + $TextContentRows = @() + if ($TextTruncate) { + foreach ($row in $TextContent) { + $TextContentRows += $row.PadRight($TextContentWidth).Substring(0,$TextContentWidth) + } + } else { + for ($i = 0; $i -lt $TextContent.Count; $i++) { + $row = $TextContent[$i] + $paddedLine = "".PadRight($TextContentWidth - (Remove-TextStyle -Text $row).Length, ' ') + $paddedLine = "$row$paddedLine" + while ($row.Length -gt $TextContentWidth) { + $reWidth = [Math]::Min($row.Length,$TextContentWidth) -1 + $lastSpace = $row.Substring(0,$reWidth).LastIndexOf(' ') + if ($lastSpace -eq -1) { + # No spaces in $TextContentWidth of space, so just chop it off at $TextContentWidth + $TextContentRows += $row.Substring(0, $reWidth) + $row = $row.Substring($reWidth) + } else { + $TextContentRows += $row.Substring(0, $lastSpace).Trim() + $row = $row.Substring($lastSpace).Trim() + } + } + $TextContentRows += $row + } + } + + + for ($i = 0; $i -lt $_marginTop; $i++) { + $output += "" + } + # draw top line + $output += "$leftMargin$_borderColorTop$leftTopCorner$topLine$rightTopCorner$ResetColor$rightMargin" + + if ($TitleText.Count -gt 0) { + foreach ($titleLine in $TitleText) { + $paddedLine = "" + if ($TextAlignment -eq 'Left') { + $paddedLine = "".PadRight($TextContentWidth - (Remove-TextStyle -Text $titleLine).Length, ' ') + $paddedLine = "$titleLine$paddedLine" + } elseif ($TextAlignment -eq 'Center') { + $rowLength = (Remove-TextStyle -Text $titleLine).Length + $rowDifference = $TextContentWidth - $rowLength + $rowLead = [Math]::Floor($rowDifference / 2) + $rowTrail = $rowDifference - $rowLead + $prefix = "".PadRight($rowLead) + $suffix = "".PadRight($rowTrail) + $paddedLine = "$prefix$row$suffix" + } elseif ($TextAlignment -eq 'Right') { + $paddedLine = "".PadRight($TextContentWidth - (Remove-TextStyle -Text $titleLine).Length, ' ') + $paddedLine = "$paddedLine$titleLine" + } else { + # Justify text + $paddedLine = Format-TextJustified -MaxWidth $TextContentWidth -Words $TitleText + } + $output += "$leftMargin$_borderColorLeft$leftBorderCharacters$ResetColor$_backgroundColor$leftPadding$ResetColor$_backgroundColor$_textColor$($PSStyle.Bold)$paddedLine$ResetColor$_backgroundColor$rightPadding$ResetColor$_borderColorRight$rightBorderCharacters$ResetColor$rightMargin" + } + $lineWidth = $boxWidth + if (![string]::IsNullOrWhiteSpace($leftBorderCharacters)) { + $lineWidth += 1 + } + if (![string]::IsNullOrWhiteSpace($rightBorderCharacters)) { + $lineWidth += 1 + } + $lineStyle = @{ + WithEndcaps = $true + Color = $BorderColorTop + Width = $lineWidth + WithBoldEndcaps = $BorderTop -contains 'Bold' + Style = $BorderTop + } + $output += Show-Line -WithEndcaps -Color $BorderColorTop -Width $lineWidth -WithBoldEndcaps + } + + # render paddingTop + for ($i = 0; $i -lt $_paddingTop; $i++) { + $output += "$leftMargin$_borderColorLeft$leftBorderCharacters$ResetColor$_backgroundColor$leftPadding$paddingLine$rightPadding$ResetColor$_borderColorRight$rightBorderCharacters$ResetColor$rightMargin" + } + + # render content + <# + foreach ($row in $TextContentRows) { + $paddedLine = "" + if ($TextAlignment -eq 'Left') { + $paddedLine = "".PadRight($TextContentWidth - (Remove-TextStyle -Text $titleLine).Length, ' ') + $paddedLine = "$row$paddedLine" + } elseif ($TextAlignment -eq 'Center') { + $rowLength = (Remove-TextStyle -Text $row).Length + $rowDifference = $TextContentWidth - $rowLength + $rowLead = [Math]::Floor($rowDifference / 2) + $rowTrail = $rowDifference - $rowLead + $prefix = "".PadRight($rowLead) + $suffix = "".PadRight($rowTrail) + $paddedLine = "$prefix$row$suffix" + } elseif ($TextAlignment -eq 'Right') { + $paddedLine = "".PadRight($TextContentWidth - (Remove-TextStyle -Text $titleLine).Length, ' ') + $paddedLine = "$paddedLine$row" + } else { + # Justify text + $paddedLine = Format-TextJustified -MaxWidth $TextContentWidth -Words $row + } + $output += "$leftMargin$_borderColorLeft$leftBorderCharacters$ResetColor$_backgroundColor$leftPadding$ResetColor$_backgroundColor$_textColor$paddedLine$ResetColor$_backgroundColor$rightPadding$ResetColor$_borderColorRight$rightBorderCharacters$ResetColor$rightMargin" + } + #> + foreach ($row in $contentRows) { + $paddedLine = "" + if ($TextAlignment -eq 'Left') { + $paddedLine = $row.PadRight($contentWidth, ' ') + } elseif ($TextAlignment -eq 'Center') { + $rowLength = $row.Length + $rowDifference = $contentWidth - $row.Length + $rowLead = [Math]::Floor($rowDifference / 2) + $rowTrail = $rowDifference - $rowLead + $prefix = "".PadRight($rowLead) + $suffix = "".PadRight($rowTrail) + $paddedLine = "$prefix$row$suffix" + } elseif ($TextAlignment -eq 'Right') { + $paddedLine = $row.PadLeft($contentWidth, ' ') + } else { + # Justify text + $paddedLine = Format-TextJustified -MaxWidth $contentWidth -Words $row + } + $output += "$leftMargin$_borderColorLeft$leftBorderCharacters$ResetColor$_backgroundColor$leftPadding$ResetColor$_backgroundColor$_textColor$paddedLine$ResetColor$_backgroundColor$rightPadding$ResetColor$_borderColorRight$rightBorderCharacters$ResetColor$rightMargin" + } + + # render paddingBottom + for ($i = 0; $i -lt $_paddingBottom; $i++) { + $output += "$leftMargin$_borderColorLeft$leftBorderCharacters$ResetColor$_backgroundColor$leftPadding$paddingLine$rightPadding$ResetColor$_borderColorRight$rightBorderCharacters$ResetColor$rightMargin" + } + # draw bottom line + $output += "$leftMargin$_borderColorBottom$leftBottomCorner$bottomLine$rightBottomCorner$ResetColor$rightMargin" + for ($i = 0; $i -lt $_marginBottom; $i++) { + $output += "" + } + + $output +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-CommandDefinition.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-CommandDefinition.ps1 new file mode 100644 index 0000000..99a7973 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-CommandDefinition.ps1 @@ -0,0 +1,115 @@ +function Show-CommandDefinition { +<# +.SYNOPSIS + Writes to screen the definition for a cmdlet, function, etc. + +.PARAMETER Command + [string] Mandatory. Specifies what command's definition will be written. + +.PARAMETER Clipboard + [switch] Optional. If used, also copy command information to clipboard. + +.PARAMETER Detail + [switch] Optional. If used, provides additional info about command. +#> + [CmdletBinding()] + [OutputType([string[]])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateCount(1,1)] + [ValidatePattern('[A-Za-z0-9\-\.\s\\:]')] + [string[]]$Command, + [Parameter(Mandatory = $false)] + [switch]$Clipboard, + [Parameter(Mandatory = $false)] + [switch]$Detail + ) + + + $fullOutput="" # Full output + $outputHeader="" # Command details + $outputBody="" # Command text + + # A basic check to make sure we're not getting more than one command somehow. + if ( (get-command $Command -ErrorAction SilentlyContinue).count -gt 1 ) { + # Passed in a wildcard or otherwise returning multiple commands. This will not work. + Write-Error "Error: This cmdlet only accepts single command arguments with no wildcards." + } else { + try { + $WarningPreference="SilentlyContinue" + # Get all command info + $commandInfo=(Get-Command $Command) + + # Get the specific command info for header and command content + $outputHeader+="Command Name: $($commandInfo.Name)`n" + $outputHeader+="Command Type: $($commandInfo.CommandType)`n" + if ( $commandInfo.CommandType -eq "Alias" ) { + # Do other weird stuff + $outputHeader+="Resolved Command: $($commandInfo.ResolvedCommand)`n" + # Reset $commandInfo values to resolved command info + $commandInfo=(Get-Command $commandInfo.ResolvedCommand) + $outputHeader+="Command Source: $($commandInfo.Source)`n" + $outputHeader+="Version: $($commandInfo.Version)`n" + # Get the alias's command definition + $outputBody=$commandInfo.Definition + $fullOutput=$outputHeader,$outputBody + } elseif ( $commandInfo.CommandType -eq "ExternalScript" -or $commandInfo.CommandType -eq "Application" ) { + $outputHeader+="Command Path: $($commandInfo.Path)`n" + # Get the script/app definition + $outputBody=$commandInfo.Definition + if ( $commandInfo.CommandType -eq "Application" ) { + $outputHeader+="Product Name: $($commandInfo.FileVersionInfo.ProductName)`n" + $outputHeader+="Version: $($commandInfo.Version)`n" + $fullOutput=$outputHeader + } else { + $fullOutput=$outputHeader,$outputBody + } + } elseif ( $commandInfo.CommandType -eq "Function" -or $commandInfo.CommandType -eq "Cmdlet" ) { + $outputHeader+="Command Source: $($commandInfo.Source)`n" + $outputHeader+="Version: $($commandInfo.Version)`n" + # Get function/cmdlet definition + $outputBody=$commandInfo.Definition + $fullOutput=$outputHeader,$outputBody + } else { + # It's some other thing, a Filter, workflow, something. + if ( $null -ne $commandInfo.Path ) { + $outputHeader+="Command Path: $($commandInfo.Path)`n" + } + if ( $null -ne $commandInfo.Source) { + $outputHeader+="Command Source: $($commandInfo.Source)`n" + } + if ( $null -ne $commandInfo.Version ) { + $outputHeader+="Command Version: $($commandInfo.Version)`n" + } + # Get the definition + $outputBody=$commandInfo.Definition + $fullOutput=$outputHeader,$outputBody + } + + if ( $Clipboard ) { + # Clear the clipboard, output info to clipboard + $null | Set-Clipboard + + if ( $Detail ) { + $fullOutput | Set-Clipboard + $fullOutput + } else { + $outputBody | Set-Clipboard + $outputBody + } + } else { + # Just write to screen + if ( $Detail ) { + $fullOutput + } else { + $outputBody + } + } + } catch { + Write-Error "Error getting command definition: $($_.exception.message)" + } finally { + $WarningPreference="Continue" + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-Help.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-Help.ps1 new file mode 100644 index 0000000..b9345df --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-Help.ps1 @@ -0,0 +1,158 @@ +function Show-Help { + [CmdletBinding()] + param ( + [Alias('Command')] + [Alias('CommandName')] + $Name + ) + + $allHelp = Get-Help -Name $Name + + foreach ($help in $allHelp) { + $titleLines = @() + $textLines = @() + + $helpName = $help.details.name + $verb = $help.details.verb + if ([string]::IsNullOrWhiteSpace($verb)) { + $verb = ($helpName -split '-')[0] + } + + $maxWidth = (Get-ConsoleDisplayWidth) - 30 + if ($maxWidth -lt 50) { + $maxWidth = Get-ConsoleDisplayWidth + } + + $verbType = (Get-Verb -Verb $verb).Group + + $moduleDisplay = "" + if (![string]::IsNullOrWhiteSpace($help.ModuleName)) { + $moduleDisplay = "$($PSStyle.ForegroundColor.White)$($help.ModuleName)\$($PSStyle.Reset)" + } + + $titleLines += "$($help.category) $moduleDisplay$($PSStyle.Bold)$helpName$($PSStyle.BoldOff) ($verb is a $verbType type Verb)" + + $titleLines += "`t$($help.Synopsis)" + + $description = Format-TextWrapToDisplay -MaxWidth $maxWidth -Text ($help.description.text -join ' ') + if ($description.Count -gt 0) { + $textLines += "$($PSStyle.ForegroundColor.White)Description:$($PSStyle.Reset)" + foreach($line in $description) { + $textLines += "`t$line" + } + } + + foreach ($inputType in $help.inputTypes.inputType) { + $type = Convert-TypeForHelpDisplay -TypeName $inputType.type.name + $description = Format-TextWrapToDisplay -MaxWidth $maxWidth -Text $inputType.description.Text + $textLines += "$($PSStyle.ForegroundColor.White)InputType:$($PSStyle.Reset) $($PSStyle.ForegroundColor.DarkGray)[$type]$($PSStyle.Reset)" + foreach($line in $description) { + $textLines += "`t$line" + } + } + + foreach ($returnValue in $help.returnValues.returnValue) { + $type = Convert-TypeForHelpDisplay -TypeName $returnValue.type.name + $description = Format-TextWrapToDisplay -MaxWidth $maxWidth -Text $returnValue.description.Text + $textLines += "$($PSStyle.ForegroundColor.White)Returns:$($PSStyle.Reset) $($PSStyle.ForegroundColor.DarkGray)[$type]$($PSStyle.Reset)" + foreach($line in $description) { + $textLines += "`t$line" + } + } + + $textLines += " " + + foreach ($parameter in $help.parameters.parameter) { + $parameterName = $parameter.Name + $type = Convert-TypeForHelpDisplay -TypeName $parameter.type.name + + $parameterValues = $help.syntax.syntaxItem.parameter.Where({$_.Name -eq $parameterName})[0].parameterValueGroup.parameterValue + $globbing = $parameter.globbing + $parameterValue = $parameter.parameterValue + $defaultValue = $parameter.defaultValue + if ($defaultValue -eq 'none') { + $defaultValue = '' + } + $aliases = ($parameter.aliases -split ',' | Foreach-Object { $_.Trim() }).Where({$_ -ne 'none' -and ![string]::IsNullOrWhiteSpace($_)}) + $required = $parameter.required + if ($required -eq 'true') { + $required = "*" + } else { + $required = " " + } + $defaultValue = "" + if (![string]::IsNullOrWhiteSpace($defaultValue)) { + $defaultValue = "(=$defaultValue)" + } + $wildcardText = "" + if ($globbing -eq 'true') { + $wildcardText = " $($PSStyle.ForegroundColor.LightCyan)(allows wildcards)$($PSStyle.Reset)" + } + + $textLines += "$($PSStyle.ForegroundColor.White)Parameter:$($PSStyle.Reset) $required $($PSStyle.ForegroundColor.DarkGray)[$type]$($PSStyle.Reset) $($PSStyle.Bold)$parameterName$($PSStyle.BoldOff)$defaultValue$wildcardText" + + $usage = @("$($PSStyle.Bold)-$parameterName$($PSStyle.BoldOff)") + foreach ($alias in $aliases) { + $usage += "|-$alias" + } + $usage = $usage -join '' + $textLines += "$($PSStyle.ForegroundColor.White)Usage:$($PSStyle.Reset) $usage" + + if ($null -ne $parameterValues) { + $values = $parameterValues -join ',' + $textLines += "$($PSStyle.ForegroundColor.White)Values:$($PSStyle.Reset) [$values]" + } + + $pipelineInput = $parameter.pipelineInput + $pipelineInputByValue = $pipelineInput -contains "ByValue" + $pipelineInputByName = $pipelineInput -contains "ByPropertyName" + if ($pipelineInputByName) { + $textLines += "Accepts Pipeline Input By Name" + } + if ($pipelineInputByValue) { + $textLines += "Accepts Pipeline Input By Value" + } + + $position = $parameter.position.Trim() + if ($position -match "^[0-9]+$") { + $textLines += "$($PSStyle.ForegroundColor.White)Position:$($PSStyle.Reset) $position" + } + + $description = ($parameter.description.text -join ' ') + if (![string]::IsNullOrWhiteSpace($description)) { + $descriptionLines = Format-TextWrapToDisplay -MaxWidth $maxWidth -Text $description + foreach($line in $descriptionLines) { + $textLines += "`t$line" + } + } + } + + foreach ($link in $help.relatedLinks.navigationLink) { + $linkText = $link.linkText + if ([string]::IsNullOrWhiteSpace($link.uri)) { + $verbProbable = ($linkText -split '-')[0] + if ($verbProbable -eq (Get-Verb -Verb $verbProbable).Verb) { + $commandProbable = Get-Command -Name $linkText + if ($null -ne $commandProbable) { + $textLines += "See also: $($commandProbable.ModuleName)\$linkText" + } else { + $textLines += "Related: $linkText" + } + } else { + $textLines += "Related: $linkText" + } + } else { + $textLines += "Link: $linkText" + $textLines += "`t`t$($link.uri)" + } + } + + foreach ($alert in $help.alertSet) { + if (![string]::IsNullOrWhiteSpace($alert.text)) { + $textLines += "AlertSet: $($alert.text)" + } + } + + Show-Box -TextContent $textLines -TitleText $titleLines -Padding 1 -TextTruncate + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-Line.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-Line.ps1 new file mode 100644 index 0000000..50232f8 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-Line.ps1 @@ -0,0 +1,131 @@ +function Show-Line { + param ( + [Parameter(Mandatory = $false)] + [string]$Width = '100%', + [Parameter(Mandatory = $false)] + [ValidateSet('Line','Bold','Dashed','BoldDashed','DashedNarrow','BoldDashedNarrow','DashedWide','BoldDashedWide','LeftHalf','RightHalf','BoldLeftHalf','BoldRightHalf','BoldRightThinLeft','BoldLeftThinRight','Double')] + [string]$Style = 'Line', + [Parameter(Mandatory = $false)] + [ValidateSet('Black','Blue','Cyan','DarkGray','Green','LightBlue','LightCyan','LightGray','LightGreen','LightMagenta','LightRed','LightYellow','Magenta','Red','White','Yellow','Default')] + [string]$Color = 'Default', + [int]$Padding, + [int]$PaddingStart, + [int]$PaddingEnd, + [switch]$WithEndcaps, + [Parameter(Mandatory = $false)] + [ValidateSet('Line','Dashed','DashedNarrow','DashedWide','HalfTop','HalfBottom','Bold','BoldDashed','BoldDashedNarrow','BoldDashedWide','BoldTopHalf','BoldTopThinBottom','BoldBottomHalf','BoldBottomThinTop','Double')] + [string]$EndcapStyle, + [switch]$WithBoldEndcaps, # This provided for simplicity and ease of use + [switch]$WithLeftEndcap, + [Parameter(Mandatory = $false)] + [ValidateSet('Line','Dashed','DashedNarrow','DashedWide','HalfTop','HalfBottom','Bold','BoldDashed','BoldDashedNarrow','BoldDashedWide','BoldTopHalf','BoldTopThinBottom','BoldBottomHalf','BoldBottomThinTop','Double')] + [string]$LeftEndcapStyle, + [switch]$WithRightEndcap, + [Parameter(Mandatory = $false)] + [ValidateSet('Line','Dashed','DashedNarrow','DashedWide','HalfTop','HalfBottom','Bold','BoldDashed','BoldDashedNarrow','BoldDashedWide','BoldTopHalf','BoldTopThinBottom','BoldBottomHalf','BoldBottomThinTop','Double')] + [string]$RightEndcapStyle + ) + + $logLead = Get-LogLeadName + $isNumberRegex = "^\d+$" + $isPercentRegex = "^\d+%$" + $ResetColor = $PSStyle.Reset + + $_style = $PSBox.Horizontal.$Style + + if ($WithBoldEndcaps) { + $WithEndcaps = $true + } + + if ($WithEndcaps) { + $WithLeftEndcap = $true + $WithRightEndcap = $true + } + + if (![string]::IsNullOrWhiteSpace($EndcapStyle)) { + if ([string]::IsNullOrWhiteSpace($LeftEndcapStyle)) { + $LeftEndcapStyle = $EndcapStyle + } + + if ([string]::IsNullOrWhiteSpace($RightEndcapStyle)) { + $RightEndcapStyle = $EndcapStyle + } + } + + $useBoldHorizontal = $Style -contains 'Bold' + $useDoubleHorizontal = $Style -contains 'Double' + + if (![string]::IsNullOrWhiteSpace($LeftEndcapStyle)) { + $useBoldVertical = $LeftEndcapStyle -eq 'Bold' + } + + if ($Padding -gt 0 -and $PaddingStart -eq 0) { + $PaddingStart = $Padding + } + + if ($Padding -gt 0 -and $PaddingEnd -eq 0) { + $PaddingEnd = $Padding + } + $prefix = "".PadRight($PaddingStart) + $suffix = "".PadRight($PaddingEnd) + + $screenWidth = Get-ConsoleDisplayWidth + $boxWidth = $screenWidth - $PaddingStart -$PaddingEnd + $widthIsNumber = $Width -match $isNumberRegex + $widthIsPercent = $Width -match $isPercentRegex + if ($widthIsNumber) { + $boxWidth = [int]$Width + if ($boxWidth -gt $screenWidth) { + $boxWidth = $screenWidth + } + } + if ($widthIsPercent) { + $boxWidth = [System.Math]::Round((([int]($Width.Substring(0,$Width.Length - 1)) * $boxWidth )/ 100.0)) + } + if (!$widthIsNumber -and !$widthIsPercent) { + throw "$logLead : Width parameter must be an int or a percent expressed as an integer and percent sign. Examples: 10, 25%, 100" + } + + if ($Color -eq 'Default') { + $_color = $ResetColor + } else { + $_color = $PSStyle.ForegroundColor.$Color + } + + $endcapStart = "" + $endcapEnd = "" + + if ($WithEndcaps) { + if ($useBoldHorizontal) { + if ($useBoldVertical) { + $endcapStart = $PSBox.Tee.Vertical.Left.Bold + $endcapend = $PSBox.Tee.Vertical.Right.Bold + } else { + $endcapStart = $PSBox.Tee.Vertical.Left.BoldHorizontal + $endcapend = $PSBox.Tee.Vertical.Right.BoldHorizontal + } + } elseif ($useDoubleHorizontal) { + if ($LeftEndcapStyle -eq "Double") { + $endcapStart = $PSBox.Tee.Vertical.Left.Double + $endcapStart = $PSBox.Tee.Vertical.Right.Double + } else { + $endcapStart = $PSBox.Tee.Vertical.Left.DoubleHorizontal + $endcapStart = $PSBox.Tee.Vertical.Right.DoubleHorizontal + } + $endcapend = $PSBox.Tee.Vertical.Right.Double + } else { + if ($useBoldVertical) { + $endcapStart = $PSBox.Tee.Vertical.Left.BoldVertical + $endcapend = $PSBox.Tee.Vertical.Right.BoldVertical + } else { + $endcapStart = $PSBox.Tee.Vertical.Left.Line + $endcapend = $PSBox.Tee.Vertical.Right.Line + } + } + $boxWidth = $boxWidth - 2 + } + + $body = $_style * $boxWidth + + return "$prefix$_color$endcapStart$body$endcapEnd$ResetColor$suffix" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-ListAsTable.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-ListAsTable.ps1 new file mode 100644 index 0000000..20da8e9 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-ListAsTable.ps1 @@ -0,0 +1,65 @@ +function Show-ListAsTable { +<# +.SYNOPSIS + Show a list of strings as neatly-wide across the screen as we can in same-sized columns. + +.PARAMETER List + A list of strings for display. This doesn't work with non-string items so well. + It will still try to call .ToString() on each element as required tho. + +.PARAMETER Padding + Some space between elements. +#> + param( + [Parameter(Mandatory = $true, Position = 0)] + [string[]]$List, + [int]$Padding = 4 + ) + + $maxDisplayWidth = (Get-ConsoleDisplayWidth) + + $longestWordLength = 0 + foreach($word in $List) { if ($longestWordLength -lt ($word.ToString()).Length) { $longestWordLength = ($word.ToString()).Length } } + $longestWordLength += $Padding #add some spaces + $numberOfColumns = [int]($maxDisplayWidth / $longestWordLength) + $firstColumnDepth = [int](($List.Count / $numberOfColumns) + 1) + + # If the number of items to display are less than double the number of columns + # then don't bother with trying to make columns, cos it's a short list + if ($numberOfColumns -gt ($List.Length / 2)) { + return $List + } + + $columns = [string[][]]::new($numberOfColumns) + for($iterator = 0; $iterator -lt $numberOfColumns; $iterator += 1) { + $columns[$iterator] = [string[]]::new(0) + } + $currentColumnCounter = 0 + + # Array has been initialized + # Now to populate it + + for($iterator = 0; $iterator -lt $numberOfColumns; $iterator += 1) { + # Arrange them alphabetically. Populate the 0th column with 0th-$firstColumnDepth count + # Second column is $firstColumnDepth+1 + $firstColumnDepth*2 elements etc + for($depthCounter = 0; $depthCounter -lt $firstColumnDepth; $depthCounter += 1) { + $columns[$iterator] += $List[$depthCounter + ($iterator * $firstColumnDepth)] + } + } + + # Array has been filled, now to print it + + $rows = @() + for($descender = 0; $descender -lt $firstColumnDepth; $descender += 1) { + $row = "" + for($iterator = 0; $iterator -lt $numberOfColumns; $iterator += 1) { + # Not all columns have the same count + if (!!$columns[$iterator][$descender]) { + $row += $columns[$iterator][$descender].PadRight($longestWordLength) + } + } + $rows += " " + $row.Trim() + } + + Write-Host ($rows -join [System.Environment]::NewLine) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-ToastNotification.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-ToastNotification.ps1 new file mode 100644 index 0000000..1604834 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-ToastNotification.ps1 @@ -0,0 +1,139 @@ +function Show-ToastNotification { +<# +.SYNOPSIS + Used to send a notification message to the screen, + especially useful in automated scripts or scripts + that could use feedback faster than waiting on a program to exit + +.DESCRIPTION + This will use the [Windows.UI.Notifications.ToastNotificationManager] to output a small toast window on screen. + Toasts are the notifications that pop up and then disappear after a short period of time, or that stay persistent on screen. + +.PARAMETER ToastText + Maximum length 220 characters without a title, 150 charaters with a title. Will trim to the first 150/220 chars. + No trimming occurs if the system is non-interactive, and instead the toast is written to standard output like so: + TOAST===TOAST===TOAST===TOAST===TOAST===TOAST + TITLE: Sample Toast + This is a sample toast notification that was + processed on a text based system + ============================================= +#> + [CmdletBinding(DefaultParameterSetName = "Seconds")] + [OutputType([void])] + Param ( + [Parameter(Mandatory = $false)] + [string]$ToastTitle = "", + [Parameter(Mandatory = $false)] + [string]$AppId = "PowerShell Toast Window", + [Parameter(Mandatory = $false)] + [string]$Tag = "", + [Parameter(Mandatory = $false)] + [string]$Group = "", + [Parameter(ParameterSetName = "Seconds")] + [int]$Seconds = 15, + [Parameter(ParameterSetName = "Milliseconds")] + [int]$Milliseconds = 0, + [switch]$NoTimeout, + [switch]$ForceTryDisplay, + [Parameter(ValueFromPipeline = $true, Mandatory = $false)] + [Alias('Text')] + [Alias('Content')] + [string]$ToastText, + [switch]$ShowInActionCenter + ) + + $toastTextEmpty = [string]::IsNullOrWhiteSpace($ToastText) + $toastTitleEmpty = [string]::IsNullOrWhiteSpace($ToastTitle) + + if ($toastTextEmpty) { + $ToastText = "" + } + + if (Test-IsMacOSPlatform) { + $ToastText = $ToastText.Replace('"',"'") + if ($toastTitleEmpty) { + $ToastTitle = "PowerShell Notification" + } + osascript -e "Display notification \`"$ToastText\`" with title \`"$ToastTitle\`"" + return + } + + if (!$ForceTryDisplay -and !(Test-IsWindowsDesktop) -and !(Test-IsInteractiveSession)) { + Write-Verbose "Toast notification requested, but the session is non-interactive and/or not running on Windows. Toast support only provided for Windows." + Write-Host "TOAST===TOAST===TOAST===TOAST===TOAST===TOAST" + if (!$toastTitleEmpty) { + Write-Host "TITLE: $ToastTitle" + } + $wrappedLines = (Format-TextWrapToDisplay -Width 45 -Text $ToastText) + foreach($line in $wrappedLines) { + Write-Host $line + } + Write-Host "=============================================" + return + } + + $expirationTime = $null + + if ($PSCmdlet.ParameterSetName -eq "Seconds") { + $Milliseconds = $Seconds * 1000 + } + + if ($Milliseconds -le 0) { + $NoTimeout = $true + } + + if (!$NoTimeout) { + $expirationTime = [DateTimeOffset]::Now.AddMilliseconds($Milliseconds) + } + + $toastTextTrim = 220 + + if ($toastTitleEmpty) { + $toastTextTrim = 150 + } + + $ToastText = $ToastText.PadRight($toastTextTrim).Substring(0,$toastTextTrim).Trim() + + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null + $Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02) + if ($toastTitleEmpty) { + $Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01) + } + + $RawXml = [xml] $Template.GetXml() + Write-Host $RawXml.OuterXml + if ($toastTitleEmpty) { + ($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq "1"}).AppendChild($RawXml.CreateTextNode($ToastText)) > $null + } else { + ($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq "1"}).AppendChild($RawXml.CreateTextNode($ToastTitle)) > $null + ($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq "2"}).AppendChild($RawXml.CreateTextNode($ToastText)) > $null + } + + $SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument + $SerializedXml.LoadXml($RawXml.OuterXml) + + $Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml) + if (![string]::IsNullOrWhiteSpace($Tag)) { + $Toast.Tag = $Tag + } + + if (![string]::IsNullOrWhiteSpace($Group)) { + $Toast.Group = $Group + } + + if ($null -ne $expirationTime) { + $Toast.ExpirationTime = $expirationTime + } + + if ($ShowInActionCenter) { + $RegPath = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings' + if (!(Test-Path -Path "$RegPath\$AppId")) { + if($PSCmdlet.ShouldProcess("creating: '$RegPath\$AppId' with property 'ShowInActionCenter' set to '1' (DWORD)")) { + (New-Item -Path "$RegPath\$AppId" -Force) | Out-Null + (New-ItemProperty -Path "$RegPath\$AppId" -Name 'ShowInActionCenter' -Value 1 -PropertyType 'DWORD') | Out-Null + } + } + } + + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($Toast) +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Show-Verbs.ps1 b/Modules/Cole.PowerShell.Developer/Public/Show-Verbs.ps1 new file mode 100644 index 0000000..8c5710e --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Show-Verbs.ps1 @@ -0,0 +1,17 @@ +function Show-Verbs { +<# +.SYNOPSIS + Show the verbs so I can figure out which one I want +#> + param( + [Parameter(Mandatory = $false, Position = 0)] + [string]$VerbPartial + ) + + $verbs = (Get-Verb).Verb + [array]$matchedVerbs = $verbs.Where({$_ -match $VerbPartial}) | Sort-Object + + # TODO: If verbs list is empty, give a better error message + + Show-ListAsTable $matchedVerbs +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-ArrayBinding.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-ArrayBinding.ps1 new file mode 100644 index 0000000..caa180d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-ArrayBinding.ps1 @@ -0,0 +1,81 @@ +function Test-ArrayBinding { +<# +.SYNOPSIS + This function is a test-bed for testing how some array stuff works + There is no useful reason to call this outside of testing +#> + + function giveMeNull { return $null } + function giveMeOne { return 1 } + function giveMeMany { return 1,2,3,4,5 } + function giveMeSomeNulls { return 1,2,$null,$null,5 } + + Write-Host "Doing some assigns" + $nullArray = [array](giveMeNull) + $nullAt = @(giveMeNull) + + $oneArray = [array](giveMeOne) + $oneAt = @(giveMeOne) + + $manyArray = [array](giveMeMany) + $manyAt = [array](giveMeMany) + + $someArray = [array](giveMeSomeNulls) + $someAt = [array](giveMeSomeNulls) + + Write-Host "doing some compares" + Write-Host "`$null -eq : Straight compare to null`nTest-IsCollectionNullOrEmpty : Determines if the array is null or .Count is 0`nAny : Tests if this is not a valid array or any elements are null`n" + + Write-Host "giveMeNull - Array" + $eqNullArray = ($null -eq $nullArray) + $ticNullArray = (Test-IsCollectionNullOrEmpty $nullArray) + $anyNullArray = (Any $nullArray) + Write-Host "`$null -eq : $eqNullArray`nTest-IsCollectionNullOrEmpty : $ticNullArray`nAny : $anyNullArray`n" + + Write-Host "giveMeNull - At" + $eqNullAt = ($null -eq $nullAt) + $ticNullAt = (Test-IsCollectionNullOrEmpty $nullAt) + $anyNullAt = (Any $nullAt) + Write-Host "`$null -eq : $eqNullAt`nTest-IsCollectionNullOrEmpty : $ticNullAt`nAny : $anyNullAt`n" + + Write-Host "giveMeOne - Array" + $eqOneArray = ($One -eq $OneArray) + $ticOneArray = (Test-IsCollectionNullOrEmpty $OneArray) + $anyOneArray = (Any $OneArray) + Write-Host "`$null -eq : $eqOneArray`nTest-IsCollectionNullOrEmpty : $ticOneArray`nAny : $anyOneArray`n" + + Write-Host "giveMeOne - At" + $eqOneAt = ($One -eq $OneAt) + $ticOneAt = (Test-IsCollectionNullOrEmpty $OneAt) + $anyOneAt = (Any $OneAt) + Write-Host "`$null -eq : $eqOneAt`nTest-IsCollectionNullOrEmpty : $ticOneAt`nAny : $anyOneAt`n" + + Write-Host "giveMeMany - Array" + $eqManyArray = ($Many -eq $ManyArray) + $ticManyArray = (Test-IsCollectionNullOrEmpty $ManyArray) + $anyManyArray = (Any $ManyArray) + Write-Host "`$null -eq : $eqManyArray`nTest-IsCollectionNullOrEmpty : $ticManyArray`nAny : $anyManyArray`n" + + Write-Host "giveMeMany - At" + $eqManyAt = ($Many -eq $ManyAt) + $ticManyAt = (Test-IsCollectionNullOrEmpty $ManyAt) + $anyManyAt = (Any $ManyAt) + Write-Host "`$null -eq : $eqManyAt`nTest-IsCollectionNullOrEmpty : $ticManyAt`nAny : $anyManyAt`n" + + Write-Host "giveMeSomeNulls - Array" + $eqSomeNullsArray = ($SomeNulls -eq $SomeNullsArray) + $ticSomeNullsArray = (Test-IsCollectionNullOrEmpty $SomeNullsArray) + $anySomeNullsArray = (Any $SomeNullsArray) + Write-Host "`$null -eq : $eqSomeNullsArray`nTest-IsCollectionNullOrEmpty : $ticSomeNullsArray`nAny : $anySomeNullsArray`n" + + Write-Host "giveMeSomeNulls - At" + $eqSomeNullsAt = ($SomeNulls -eq $SomeNullsAt) + $ticSomeNullsAt = (Test-IsCollectionNullOrEmpty $SomeNullsAt) + $anySomeNullsAt = (Any $SomeNullsAt) + Write-Host "`$null -eq : $eqSomeNullsAt`nTest-IsCollectionNullOrEmpty : $ticSomeNullsAt`nAny : $anySomeNullsAt`n" + # 123456789012345678912345678901 12345678901234 12345678901234 12345678901234 12345678901234 + Write-Host "Test name | F null F | T one T | T many T | F some null F |" + Write-Host "Test-IsColllectionNullOrEmpty | $($eqNullArray.ToString().PadRight(5)) $($eqNullAt.ToString().PadRight(5)) | $($eqOneArray.ToString().PadRight(5)) $($eqOneAt.ToString().PadRight(5)) | $($eqManyArray.ToString().PadRight(5)) $($eqManyAt.ToString().PadRight(5)) | $($eqSomeNullsArray.ToString().PadRight(5)) $($eqSomeNullsAt.ToString().PadRight(5)) |" + Write-Host "Test-IsColllectionNullOrEmpty | $($ticNullArray.ToString().PadRight(5)) $($ticNullAt.ToString().PadRight(5)) | $($ticOneArray.ToString().PadRight(5)) $($ticOneAt.ToString().PadRight(5)) | $($ticManyArray.ToString().PadRight(5)) $($ticManyAt.ToString().PadRight(5)) | $($ticSomeNullsArray.ToString().PadRight(5)) $($ticSomeNullsAt.ToString().PadRight(5)) |" + Write-Host "Any (Test-IsArrayValid) | $($anyNullArray.ToString().PadRight(5)) $($anyNullAt.ToString().PadRight(5)) | $($anyOneArray.ToString().PadRight(5)) $($anyOneAt.ToString().PadRight(5)) | $($anyManyArray.ToString().PadRight(5)) $($anyManyAt.ToString().PadRight(5)) | $($anySomeNullsArray.ToString().PadRight(5)) $($anySomeNullsAt.ToString().PadRight(5)) |" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForPackages.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForPackages.ps1 new file mode 100644 index 0000000..a445821 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForPackages.ps1 @@ -0,0 +1,66 @@ +Function Test-CallProgetForPackages { + param ( + [ArgumentCompleter({ + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $p = $fakeBoundParameters.Package + $p = "$p".ToLower() + $feed = $fakeBoundParameters.Feed + if ([string]::IsNullOrWhiteSpace($feed)) { + $feed = 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/' + } + if ($feed.EndsWith('/')) { + $feed = $feed.TrimEnd("/") + } + $response = Invoke-RestMethod "$feed/Packages()?`$filter=startswith(tolower(Id),'$p') and IsAbsoluteLatestVersion&`$top=1000" + $packages = $response.title."#text" + return $packages | Sort-Object + })] + [Alias("Id")] + [Alias("PackageId")] + [Alias("PackageName")] + [Alias("Name")] + [string]$Package, + [ArgumentCompleter({ + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $p = $fakeBoundParameters.Package + $p = "$p".ToLower() + $feed = $fakeBoundParameters.Feed + if ([string]::IsNullOrWhiteSpace($feed)) { + $feed = 'https://packagerepo.orb.alkamitech.com/nuget/choco.dev/' + } + if ($feed.EndsWith('/')) { + $feed = $feed.TrimEnd("/") + } + $response = Invoke-RestMethod "$feed/Packages()?`$filter=tolower(Id) eq '$p'&`$orderby=Version desc&`$top=1000" + Write-Host "Invoke-RestMethod `"$feed/Packages()?`$filter=tolower(Id) eq '$p'&`$orderby=Version desc&`$top=1000`"" + $versions = $response.properties.Version + return $versions + })] + [Alias("V")] + [Alias("PackageVersion")] + [string]$Version, + [ArgumentCompleter({ + param ( $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters ) + $configPath = Join-Path -Path (Get-ChocolateyInstallPath) -ChildPath "config\chocolatey.config" + if (Test-Path -Path $configPath) { + $config = [xml](Get-Content -Path $configPath) + return ($config.chocolatey.sources.source | Where-Object {$_.Disabled -ne 'true'}).value + } + return @('https://packagerepo.orb.alkamitech.com/nuget/choco.dev/') + })] + [Alias("Source")] + [string]$Feed + ) +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForVersions.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForVersions.ps1 new file mode 100644 index 0000000..3ee7f8b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-CallProgetForVersions.ps1 @@ -0,0 +1,9 @@ +Function Test-CallProgetForVersions { + param ( + [ArgumentCompleter({ + $response = invoke-webrequest 'https://packagerepo.orb.alkamitech.com/feeds/choco.dev/Alkami.MachineSetup.SDK/versions/all' + $versions = $response.Links.innerText.Where({$null -ne $PSItem -and $PSItem.StartsWith('2')}) + return $versions | ForEach-Object { $PSItem } + })]$Version + ) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-CustomSuppressMessage.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-CustomSuppressMessage.ps1 new file mode 100644 index 0000000..1942a0f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-CustomSuppressMessage.ps1 @@ -0,0 +1,10 @@ +function Test-CustomSuppressMessage { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami_Module_NoCommandWithRetry', '', Scope='Function', Justification="We can't wrap the internals because X")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami_Module_NoCommandWithRetry1', '', Scope='Function', Justification="We can't wrap the internals because X")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami_Module_NoCommandWithRetry2', '', Scope='Function', Justification="We can't wrap the internals because X")] + param ( + ) + + Write-Host "hmm, ok" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-DoubleParameterSets.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-DoubleParameterSets.ps1 new file mode 100644 index 0000000..8d07f74 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-DoubleParameterSets.ps1 @@ -0,0 +1,20 @@ +function Test-DoubleParameterSets { + param( + [Parameter(Mandatory = $true, ParameterSetName = "switch1")] + [Parameter(Mandatory = $false, ParameterSetName = "switch3")] + [Parameter(Mandatory = $false, ParameterSetName = "switch4")] + [switch]$switch1, + [Parameter(Mandatory = $true, ParameterSetName = "switch2")] + [Parameter(Mandatory = $false, ParameterSetName = "switch3")] + [Parameter(Mandatory = $false, ParameterSetName = "switch4")] + [switch]$switch2, + [Parameter(Mandatory = $true, ParameterSetName = "switch1")] + [Parameter(Mandatory = $true, ParameterSetName = "switch2")] + [Parameter(Mandatory = $true, ParameterSetName = "switch3")] + [switch]$switch3, + [Parameter(Mandatory = $true, ParameterSetName = "switch1")] + [Parameter(Mandatory = $true, ParameterSetName = "switch2")] + [Parameter(Mandatory = $true, ParameterSetName = "switch4")] + [switch]$switch4 + ) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-InvocationOfCommand.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-InvocationOfCommand.ps1 new file mode 100644 index 0000000..16790c1 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-InvocationOfCommand.ps1 @@ -0,0 +1,8 @@ +function Test-InvocationOfCommand { + [CmdletBinding()] + param() + + $x = Test-MyInvocationCommand + + Write-Host $x +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsArrayValid.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsArrayValid.ps1 new file mode 100644 index 0000000..028d6df --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsArrayValid.ps1 @@ -0,0 +1,34 @@ +function Test-IsArrayValid { +<# +.SYNOPSIS + Tests if an array is valid. + Valid means has contents, is an array. + No contents, or only null contents, does not constitute a valid array. +#> + [CmdletBinding()] + Param( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [object]$array + ) + + $isValid = $false + + if ($null -eq $array) { + return $isValid + } + + if ($array -is [System.Collections.IEnumerable] -and $array -isnot [string]) { + if ($array.Count -gt 0) { + for ($i = 0; $i -lt $array.Count; $i++) { + if ($null -ne $array[$i]) { + $isValid = $true + break + } + } + } + } + + return $isValid +} + +Set-Alias -Name Any -Value Test-IsArrayValid \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsCurrentAWSUserSessionValid.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsCurrentAWSUserSessionValid.ps1 new file mode 100644 index 0000000..8aa27b6 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsCurrentAWSUserSessionValid.ps1 @@ -0,0 +1,50 @@ +function Test-IsCurrentAWSUserSessionValid { +<# +.SYNOPSIS + Ensure that we are currently authenticated on the profile being used + +.PARAMETER ProfileName + The profile to use +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $false)] + [string]$ProfileName = (Get-LocalCachedAWSProfile) + ) + + $logLead = (Get-LogLeadName) + + # Always easier to just alert the user if the value they gave us was no good + Assert-ValidAWSProfileName -ProfileName $ProfileName + + $awsCredential = (Get-AWSCredentialEntries | Where-Object { $_.Profile -eq $ProfileName }) + + # If you said 'Dev' but meant 'temp-dev' then just fix it. Takes almost no time. + if ($null -eq $awsCredential) { + if ($ProfileName.StartsWith("temp-")) { + $ProfileName = $ProfileName.Replace("temp-","") + } else { + $ProfileName = "temp-$ProfileName" + } + + $awsCredential = (Get-AWSCredentialEntries | Where-Object { $_.Profile -eq $ProfileName }) + } + + if ($null -eq $awsCredential) { + Write-Error "$logLead : No matching credential found for [$ProfileName]" + } + + try { + $arn = Get-STSCallerIdentity -Select Arn -ProfileName $ProfileName + } catch { + if ("The security token included in the request is expired" -ne $_.Exception.Message) { + throw + } + return $false + } + + return $true +} + +Set-Alias -Name Test-IsAwsSessionValid -Value Test-IsCurrentAWSUserSessionValid -Scope Global \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsFalse.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsFalse.ps1 new file mode 100644 index 0000000..afdfbde --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsFalse.ps1 @@ -0,0 +1,14 @@ +function Test-IsFalse { + [CmdletBinding()] + param ( + $Value + ) + + if ($Value -is [string] -or $Value -is [int] -or $Value -is [bool]) { + return ('false',0,$false -contains $Value) + } + + return $false +} + +Set-Alias -Name IsFalse -Value Test-IsFalse \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsGitFolderRoot.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsGitFolderRoot.ps1 new file mode 100644 index 0000000..462a99c --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsGitFolderRoot.ps1 @@ -0,0 +1,16 @@ +function Test-IsGitFolderRoot { +<# +.SYNOPSIS + Test if the current folder is a git repository + +.PARAMETER Path + The file path to check. Can be presumed as the current folder. +#> + param( + $Path = ((Get-Location).Path) + ) + + $targetPath = (Join-Path -Path $Path -ChildPath ".git" -Resolve -ErrorAction Ignore) + + return (![string]::IsNullOrWhiteSpace($targetPath) -and (Test-Path -Path $targetPath)) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsInteractiveSession.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsInteractiveSession.ps1 new file mode 100644 index 0000000..25a1b82 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsInteractiveSession.ps1 @@ -0,0 +1,20 @@ +function Test-IsInteractiveSession { +<# +.SYNOPSIS + This function attempts to determine if the script is running in an interactive session + +.LINK + https://stackoverflow.com/a/62534640/109749 +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + # not including `-NonInteractive` since it apparently does nothing + # "Does not present an interactive prompt to the user" - no, it does present! + $non_interactive = '-command', '-c', '-encodedcommand', '-e', '-ec', '-file', '-f' + + # alternatively `$non_interactive [-contains|-eq] $PSItem` + return -not ([Environment]::GetCommandLineArgs() | Where-Object -FilterScript {$PSItem -in $non_interactive}) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsMacOSPlatform.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsMacOSPlatform.ps1 new file mode 100644 index 0000000..b4dee3f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsMacOSPlatform.ps1 @@ -0,0 +1,16 @@ +function Test-IsMacOSPlatform { +<# +.SYNOPSIS + Returns true if the platform is macOS +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + if (Test-IsUnixPlatform) { + return $PSVersionTable.OS -match "Darwin" + } + + return $false +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsSourceFileVersionHigherThanTarget.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsSourceFileVersionHigherThanTarget.ps1 new file mode 100644 index 0000000..8475267 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsSourceFileVersionHigherThanTarget.ps1 @@ -0,0 +1,95 @@ +Function Test-IsSourceFileVersionHigherThanTarget { +<# +.SYNOPSIS + Given two file paths, and a common filename, determine which is the latter version + +.EXAMPLE +$ProviderTargets = @('BankService','CoreService', 'NotificationService','SecurityManagementService','Radium','Nag','NagConfigurationService') + +foreach($target in $ProviderTargets) { + $probePath = (Join-Path (Join-Path "C:\Orb\" $target) "bin") + if (!(Test-Path $probePath)) { + $probePath = Split-Path $probePath + } + + $filesInProbe = @((Get-ChildItem -Path $probePath -Filter *.dll).Name) + foreach ($file in $filesInProbe) { + if (Test-IsSourceFileVersionHigherThanTarget $file $probePath "C:\Orb\Shared" $file) { + Write-Host "found $file in $probePath higher than shared" + } + } +} +#> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true,Position=0)] + [ValidateNotNullOrEmpty()] + [string]$filename, + + [Parameter(Mandatory = $true,Position=1)] + [ValidateScript( {Test-Path (Resolve-Path $_)})] + [ValidateNotNullOrEmpty()] + [string]$sourceFolderPath, + + [Parameter(Mandatory = $true,Position=2)] + [ValidateScript( {Test-Path (Resolve-Path $_)})] + [ValidateNotNullOrEmpty()] + [string]$targetFolderPath, + + [Parameter(Mandatory = $true,Position=3)] + [ValidateNotNullOrEmpty()] + [string]$packageName + ) + process { + $loglead = "File tester" + + $sourceFile = (Join-Path $sourceFolderPath $filename); + $targetFile = (Join-Path $targetFolderPath $filename); + if ((Test-Path $sourceFile) -and (Test-Path $targetFile)) { + $stringVersionFromSourceFileVersion = (Get-Item $sourceFile).VersionInfo.FileVersion + $stringVersionFromTargetFileVersion = (Get-Item $targetFile).VersionInfo.FileVersion + + ## Both strings have values, so we can read something from that + if (![System.String]::IsNullOrWhiteSpace($stringVersionFromSourceFileVersion) -and ![System.String]::IsNullOrWhiteSpace($stringVersionFromTargetFileVersion)) { + ## The version info FileVersion has a space and other content in it, so take what comes before the space. + $versionFromSourceFile = ([System.Version](((Get-Item $sourceFile).VersionInfo.FileVersion -split ' ')[0])) + $versionFromTargetFile = ([System.Version](((Get-Item $targetFile).VersionInfo.FileVersion -split ' ')[0])) + Write-Verbose "$loglead : $versionFromSourceFile : $sourceFile"; + Write-Verbose "$loglead : $versionFromTargetFile : $targetFile"; + + ## [System.Version] has .CompareTo so we can let it do the native comparison. + ## -1 = source version is lower than target + ## 0 = source version equals target + ## 1 = source version is higher than the target + $result = $versionFromSourceFile.CompareTo($versionFromTargetFile) -gt 0; + + ## If we decided we are supposed to copy the file _and_ we match this condition of the target file matching a file that already exists + ## We want to emit an error that the files are different and that we shouldn't be copying the files so we can investigate. + if (!$result -and ($targetFile -match $packageName) -and $versionFromSourceFile.CompareTo($versionFromTargetFile) -eq 0) { + if ((Get-FileHash $sourceFile).Hash -ne (Get-FileHash $targetFile).Hash) { + $message = "The files $sourceFile and $targetFile have the same version [$versionFromSourceFile] but the hashes don't match!"; + Write-Error $message; + return $false + } + } + Write-Verbose "$loglead : Result was $result"; + return $result; + } else { + Write-Warning "$loglead : Can not compare these two files because the version info is missing: $sourceFile [$stringVersionFromSourceFileVersion] - $targetFile [$stringVersionFromTargetFileVersion]" + } + } else { + if (($targetFile -match $packageName) -and (Test-Path $sourceFile) -and (Test-Path $targetFile)){ + if ((Get-FileHash $sourceFile).Hash -ne (Get-FileHash $targetFile).Hash) { + $versionFromSourceFile = ([System.Version](((Get-Item $sourceFile).VersionInfo.FileVersion -split ' ')[0])) + $message = "The files $sourceFile and $targetFile have the same version [$versionFromSourceFile] but the hashes don't match!"; + Write-Error $message; + } + } + + $result = ((Test-Path $sourceFile) -and !(Test-Path $targetFile)); + Write-Verbose "$loglead : Did the source exist and not the target? $result." + ## Honestly, we don't care about files that only exist in the final folder and not shared + return $false; + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsTrue.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsTrue.ps1 new file mode 100644 index 0000000..1f95f37 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsTrue.ps1 @@ -0,0 +1,14 @@ +function Test-IsTrue { + [CmdletBinding()] + param ( + $Value + ) + + if ($Value -is [string] -or $Value -is [int] -or $Value -is [bool]) { + return ('true',1,$true -contains $Value) + } + + return $false +} + +Set-Alias -Name IsTrue -Value Test-IsTrue \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsUnixPlatform.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsUnixPlatform.ps1 new file mode 100644 index 0000000..83aa0db --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsUnixPlatform.ps1 @@ -0,0 +1,14 @@ +function Test-IsUnixPlatform { +<# +.SYNOPSIS + Returns true if the platform is Unix + This is as opposed to Windows. Mac is a subset of Unix. +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + $platform = $PSVersionTable.Platform + return (![string]::IsNullOrWhiteSpace($platform) -and $platform -eq "Unix") +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsUserLocalAdministrator.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsUserLocalAdministrator.ps1 new file mode 100644 index 0000000..d0e39be --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsUserLocalAdministrator.ps1 @@ -0,0 +1,18 @@ +function Test-IsUserLocalAdministrator { +<# +.SYNOPSIS + Returns true if the current user is an administrator. +#> + [CmdletBinding()] + Param() + + try { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal -ArgumentList $identity + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } catch { + Write-Error -Message "Failed to determine if the current user has elevated privileges." + Write-ErrorObject -ErrorItem $PSItem + return $false + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsDesktop.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsDesktop.ps1 new file mode 100644 index 0000000..1345cd4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsDesktop.ps1 @@ -0,0 +1,12 @@ +function Test-IsWindowsDesktop { +<# +.SYNOPSIS + Returns true if the platform is Windows and this is a desktop capable OS +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + return (Test-IsWindowsPlatform) -and ($PSVersionTable.PSEdition -eq 'Desktop') +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsPlatform.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsPlatform.ps1 new file mode 100644 index 0000000..99a3368 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-IsWindowsPlatform.ps1 @@ -0,0 +1,12 @@ +function Test-IsWindowsPlatform { +<# +.SYNOPSIS + Returns true if the platform is Windows +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + return @([System.PlatformID]::Win32NT,[System.PlatformID]::Win32Windows,[System.PlatformID]::Win32S).Contains([System.Environment]::OSVersion.Platform) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-MyInvocationCommand.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-MyInvocationCommand.ps1 new file mode 100644 index 0000000..5f5c60d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-MyInvocationCommand.ps1 @@ -0,0 +1,6 @@ +function Test-MyInvocationCommand { + [CmdletBinding()] + param () + + return $MyInvocation.Line +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-SplatUsage.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-SplatUsage.ps1 new file mode 100644 index 0000000..227c919 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-SplatUsage.ps1 @@ -0,0 +1,119 @@ +function Test-SplatUsage { +<# +.SYNOPSIS + This function demonstrates how splats work based on what is passed in. + +.EXAMPLE +This example will error for too many values being supplied: +(Parses Param2 before Param1) + +$splat = @{ + Param1 = "Lorem" + Param2 = "ipsum" + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" +} +Test-SplatUsage -Param1 "thing" -Param2 "other thing" @splat + +.EXAMPLE +This example will error for too many values being supplied: +(Parses Param2 before Param1) + +$splat = @{ + Param1 = "Lorem" + Param2 = "ipsum" + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" +} +Test-SplatUsage -Param2 "thing" -Param1 "other thing" @splat + +.EXAMPLE +This example will do what you want: + +$splat = @{ + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" +} +Test-SplatUsage -Param1 "thing" -Param2 "other thing" @splat + +.EXAMPLE +Can I double-splat? Why yes, yes I can! + +$splat = @{ + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" +} +$splat2 = @{ + Param1 = "Lorem" + Param2 = "ipsum" +} +Test-SplatUsage @splat @splat2 + +.EXAMPLE +Alpha ordering test on failure conditions (test 1) + +$splat = @{ + Param1 = "Lorem" + Param2 = "ipsum" + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" + alphaTest = "wrong" + ALPHACAPS = "again wrong" +} +Test-SplatUsage -Param2 "thing" -alphacaps "wrong case" -alphaTest "supplied" -Param1 "other thing" @splat + +.EXAMPLE +Alpha ordering test on failure conditions (swapped case) + +$splat = @{ + Param1 = "Lorem" + Param2 = "ipsum" + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" + alphaTest = "wrong" + ALPHACAPS = "again wrong" +} +Test-SplatUsage -Param2 "thing" -ALPHACAPS "wrong case" -alphaTest "supplied" -Param1 "other thing" @splat + +.EXAMPLE +Alpha ordering test on failure conditions (swapped params order) + +$splat = @{ + Param1 = "Lorem" + Param2 = "ipsum" + Param3 = "dolor" + Param4 = "sit" + Param5 = "amet" + alphaTest = "wrong" + ALPHACAPS = "again wrong" +} +Test-SplatUsage -Param2 "thing" -alphaTest "supplied" -ALPHACAPS "wrong case" -Param1 "other thing" @splat +#> + param ( + $param1, + $param2, + $param3, + $param4, + $param5, + $param6, + $alphaTest, + $ALPHACAPS + ) + + Write-Host (ConvertTo-Json @{ + Param1 = $param1 + Param2 = $param2 + Param3 = $param3 + Param4 = $param4 + Param5 = $param5 + Param6 = $param6 + alphaTest = $alphaTest + ALPHACAPS = $ALPHACAPS + }) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-TerminalSupportsANSIEscape.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-TerminalSupportsANSIEscape.ps1 new file mode 100644 index 0000000..4b4e8a1 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-TerminalSupportsANSIEscape.ps1 @@ -0,0 +1,50 @@ +function Test-TerminalSupportsANSIEscapes { +<# +.SYNOPSIS + Test if the terminal being used supports ANSI escape sequences + This matters if we want to avoid spamming characters at the console when they don't benefit us. +#> + [CmdletBinding()] + [OutputType([bool])] + param ( + ) + + # Implement a runtime caching strategy so we don't keep asking the OS + if ($script:Test_TerminalSupportsANSIEscapes_set -eq $true) { + return $script:Test_TerminalSupportsANSIEscapes + } + + # Support and honor the request for no-color on output + # https://no-color.org/ + if ($null -ne (Get-EnvironmentVariable 'NO_COLOR' 6>$null 5>$null 4>$null 3>$null)) { + $script:Test_TerminalSupportsANSIEscapes_set = $true + $script:Test_TerminalSupportsANSIEscapes = $false + return $false + } + + # Start with a testable variable, default to int32->0 + $mode = 0 + # Import some headers so we can call the DLLs directly + $MethodDefinitions = @' +[DllImport("kernel32.dll", SetLastError = true)] +public static extern IntPtr GetStdHandle(int nStdHandle); +[DllImport("kernel32.dll", SetLastError = true)] +public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); +'@ + # Get a type library we can play with + $Kernel32 = Add-Type -MemberDefinition $MethodDefinitions -Name 'Kernel32' -Namespace 'Win32' -PassThru + # Ask the std_output handle if it knows what mode we are + $hConsoleHandle = $Kernel32::GetStdHandle(-11) # STD_OUTPUT_HANDLE + + if ($Kernel32::GetConsoleMode($hConsoleHandle, [ref]$mode)) { + # We were able to read the value from the std_output handle + $script:Test_TerminalSupportsANSIEscapes_set = $true + $script:Test_TerminalSupportsANSIEscapes = ($mode -ne 0) + return $script:Test_TerminalSupportsANSIEscapes + } else { + # We could not read the value, so assume false + $script:Test_TerminalSupportsANSIEscapes_set = $true + $script:Test_TerminalSupportsANSIEscapes = $false + return $false + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-WasFileModifiedWithin.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-WasFileModifiedWithin.ps1 new file mode 100644 index 0000000..f78de37 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-WasFileModifiedWithin.ps1 @@ -0,0 +1,51 @@ +function Test-WasFileModifiedWithin { +<# +.SYNOPSIS + Was this file modified within the last X period of time + +.PARAMETER Path + The path of the file + +.PARAMETER Last + Period of time to compare against + +.PARAMETER Count + The number of the time period + +.EXAMPLE + Test was this file modified in the past month +Test-WasFileModifiedWithin -Last Month -Path $somePath + +.EXAMPLE + Test was this file modified in the past two weeks +Test-WasFileModifiedWithin -Last Week -Count 2 -Path $somePath + +.OUTPUTS + Will return false if the path does not exist +#> + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Path, + [Parameter(Mandatory = $true)] + [ValidateSet('Month','Week','Day','Hour')] + [string]$Last, + [Parameter(Mandatory = $false)] + [int]$Count = 1 + ) + + if (!(Test-Path -Path $Path)) { + return $false + } + + $targetDateTime = [System.DateTime]::Now + + $targetDateTime = switch ($Last) { + 'Month' { $targetDateTime.AddMonths(-1 * $Count) } + 'Week' { $targetDateTime.AddDays(-1 * 7 * $Count) } + 'Day' { $targetDateTime.AddDays(-1 * $Count) } + 'Hour' { $targetDateTime.AddHours(-1 * $Count) } + } + + return ((Get-Item -Path $Path).LastWriteTime -lt $targetDateTime) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelper.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelper.ps1 new file mode 100644 index 0000000..86dd2d5 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelper.ps1 @@ -0,0 +1,6 @@ +function Test-WriteProgressHelper { + $activityLabel = "Test-WriteProgressHelper" + Write-ProgressHelper -Activity $activityLabel -Status "Testing child object" -PercentComplete 50 + Test-WriteProgressHelperChild + Write-Progress -Activity $activityLabel -Completed +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelperChild.ps1 b/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelperChild.ps1 new file mode 100644 index 0000000..76cca0d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Test-WriteProgressHelperChild.ps1 @@ -0,0 +1,8 @@ +function Test-WriteProgressHelperChild { + $activityLabel = "Test-WriteProgressHelperChild" + for ($i = 0; $i -lt 4; $i++) { + Write-ProgressHelper -Activity $activityLabel -Status "Sleeping 1s" -PercentComplete ($i * 25) + Start-Sleep -Seconds 1 + } + Write-Progress -Activity $activityLabel -Completed +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Trace-ActionEnd.ps1 b/Modules/Cole.PowerShell.Developer/Public/Trace-ActionEnd.ps1 new file mode 100644 index 0000000..134374f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Trace-ActionEnd.ps1 @@ -0,0 +1,21 @@ +function Trace-ActionEnd { +<# +.SYNOPSIS + End tracing the action. This is useful for gathering duration of runtime. + +.PARAMETER TraceAction + Object returned from Trace-ActionStart +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object]$TraceAction + ) + + $TraceAction.StopWatch.Stop() + $TraceAction.EndTime = [System.DateTime]::Now + $TraceAction.Duration = $TraceAction.StopWatch.Elapsed + + $global:TraceActionList.Add($TraceAction) | Out-Null +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Trace-ActionStart.ps1 b/Modules/Cole.PowerShell.Developer/Public/Trace-ActionStart.ps1 new file mode 100644 index 0000000..51673fe --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Trace-ActionStart.ps1 @@ -0,0 +1,57 @@ +function Trace-ActionStart { +<# +.SYNOPSIS + End tracing the action. This is useful for gathering duration of runtime. + +.PARAMETER ParentTraceAction + Passed in TraceAction from prior method + +.PARAMETER ActionName + Name of the specific action being traced +#> + param ( + [Parameter(Mandatory = $false)] + $ParentTraceAction = $null, + [Parameter(Mandatory = $false)] + [string]$ActionName = "Method Invocation" + ) + + process { + $callstack = Get-PSCallstack + $currentMethod = $callstack[0] + $parentMethod = $callstack[1] + + $currentMethodName = $currentMethod.FunctionName + $parentMethodName = $parentMethod.FunctionName + + $currentMethodSource = "" + $parentMethodSource = "" + + if (-not (Test-StringIsNullOrWhitespace -Value $currentMethod.ScriptName)) { + $currentMethodSource = [System.IO.Path]::GetFileNameWithoutExtension($currentMethod.ScriptName) + } + + if (-not (Test-StringIsNullOrWhitespace -Value $parentMethod.ScriptName)) { + $parentMethodSource = [System.IO.Path]::GetFileNameWithoutExtension($parentMethod.ScriptName) + } + + $traceAction = New-Object PSCustomObject -Property @{ + StartTime = [System.DateTime]::Now + EndTime = $null + StopWatch = [System.Diagnostics.Stopwatch]::StartNew() + Duration = $null + Command = $currentMethodName + ModuleName = $currentMethodSource + CalledBy = @{ + Command = $parentMethodName + ModuleName = $parentMethodSource + # Arguments = $parentMethod.InvocationInfo.BoundParameters + } + ActionName = $ActionName + # ParentTraceAction = $ParentTraceAction + # Arguments = $currentMethod.InvocationInfo.BoundParameters + } + + return $traceAction + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Trace-ClearActions.ps1 b/Modules/Cole.PowerShell.Developer/Public/Trace-ClearActions.ps1 new file mode 100644 index 0000000..c881e71 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Trace-ClearActions.ps1 @@ -0,0 +1,11 @@ +function Trace-ClearActions { +<# +.SYNOPSIS + Clear all trace actions in the global state +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Clearing global state')] + [CmdletBinding()] + param () + + $global:TraceActionList = New-Object -TypeName "System.Collections.ArrayList" +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Update-AWSAccessKey.ps1 b/Modules/Cole.PowerShell.Developer/Public/Update-AWSAccessKey.ps1 new file mode 100644 index 0000000..d4a0457 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Update-AWSAccessKey.ps1 @@ -0,0 +1,126 @@ +function Update-AWSAccessKey { +<# +.SYNOPSIS + Update the AWS access key and secret in a reasonable fashion + +.PARAMETER RoleToReplaceFor + The role you are replacing the key value for. Example: [teamcity-packer] or [Prod] + +.PARAMETER Key + The value given by the Access Key ID for AWS when choosing a new IAM Access Key + +.PARAMETER Secret + The value given by the secret for AWS when choosing a new IAM Access Key + +.PARAMETER ComputerName + Denotes the computers you wish to change the value on + +.PARAMETER Force + Will create the value if it does not exist +#> + param ( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [Alias('ProfileName')] + [string]$RoleToReplaceFor, + [Parameter(Mandatory = $true, Position = 1)] + [ValidateNotNullOrEmpty()] + [Alias('Key')] + [string]$AccessKeyId, + [Parameter(Mandatory = $true, Position = 2)] + [ValidateNotNullOrEmpty()] + [Alias('Secret')] + [string]$AccessKeySecret, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [Alias('Servers')] + [string[]]$ComputerName = (Get-CachedInstances -ProfileName temp-prod -TeamCity).Hostname, + [Parameter(Mandatory = $false)] + [Alias('Create')] + [switch]$Force + ) + + $logLead = Get-LogLeadName + + if (-not $RoleToReplaceFor.StartsWith("[")) { + $RoleToReplaceFor = "[$RoleToReplaceFor" + } + if (-not $RoleToReplaceFor.EndsWith("]")) { + $RoleToReplaceFor = "$RoleToReplaceFor]" + } + + Write-Host "$logLead : Replacing key for profile $RoleToReplaceFor with key: $AccessKeyId" + + Invoke-Command -ComputerName $ComputerName -ArgumentList ($RoleToReplaceFor , $AccessKeyId, $AccessKeySecret, $Force) -ScriptBlock { + param ($sb_role, $sb_keyId, $sb_keySecret, $sb_force) + $userPaths = @("C:\Users\ci.migrate`$\.aws\credentials", "C:\Users\dev.migrate`$\.aws\credentials", "C:\Users\qa.migrate`$\.aws\credentials", "C:\Users\jumpbox.jenkins\.aws\credentials") + foreach ($path in $userPaths) { + if (-not (Test-Path -Path $path)) { + continue + } + if ((Select-String -Path $path -Pattern $sb_keyId -SimpleMatch) -and (Select-String -Path $path -Pattern $sb_keySecret -SimpleMatch)) { + Write-Host "$env:COMPUTERNAME $path - File already matched" + return + } + if (-not (Select-String -Path $path -Pattern $sb_role -SimpleMatch) -and -not $sb_force) { + Write-Host "$env:COMPUTERNAME $path - File does not contain profile, and force was not supplied" + return + } + Write-Host "Backing up and saving $env:COMPUTERNAME $path" + Copy-Item -Path $path -Destination "$path.bak.$([Math]::Floor((Get-Date -UFormat "%s")))" + try { + $nextLine = $false + $replacedKeyId = $false + $replacedKeySecret = $false + $lines = ((Get-Content -Path $path) | ForEach-Object { + if ($nextLine) { + if ($_.Trim().StartsWith("aws_access_key_id")) { + if ($replacedKeyId -eq $true) { + throw "Attempted to set the key twice. Please confirm the file contents and try again $env:COMPUTERNAME $path" + } + Write-Output "aws_access_key_id = $sb_keyId" + $replacedKeyId = $true + if ($replacedKeySecret) { + $nextLine = $false + } + } elseif ($_.Trim().StartsWith("aws_secret_access_key")) { + if ($replacedKeySecret -eq $true) { + throw "Attempted to set the secret twice. Please confirm the file contents and try again. $env:COMPUTERNAME $path" + } + Write-Output "aws_secret_access_key = $sb_keySecret" + $replacedKeySecret = $true + if ($replacedKeyId) { + $nextLine = $false + } + } else { + Write-Output $_ + } + } else { + if ($_ -eq $sb_role) { + $nextLine = $true + } + Write-Output $_ + } + }) + + if (-not $replacedKeyId -and -not $replacedKeySecret) { + Write-Host "$env:COMPUTERNAME $path - Value for $sb_role not found" + if ($sb_force) { + # We didn't find the key, let's add it + Write-Host "$env:COMPUTERNAME $path - Value for $sb_role not found, adding" + $lines += $sb_role + $lines += "aws_access_key_id = $sb_keyId" + $lines += "aws_secret_access_key = $sb_keySecret" + } + } else { + if (($true -eq ($replacedKeyId -or $replacedKeySecret)) -and ($false -eq ($replacedKeyId -and $replacedKeySecret))) { + # only one was set to true, not both + throw "The key was not updated correctly. Please confirm the file contents and try again. $env:COMPUTERNAME $path" + } + } + + $lines | Set-Content -Path $path + } catch {} + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Update-AWSCLIAccessKey.ps1 b/Modules/Cole.PowerShell.Developer/Public/Update-AWSCLIAccessKey.ps1 new file mode 100644 index 0000000..25f86e8 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Update-AWSCLIAccessKey.ps1 @@ -0,0 +1,120 @@ +function Update-AWSCLIAccessKey { +<# +.SYNOPSIS + This function can change your AWS Access Key following the 90 day requirement. + +.PARAMETER username + The username of the current user + +.PARAMETER profile + Probably "default". That's the ... err ... default. Blame gorg whiting, he asked for this param. idk man. +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$false,Position=0)] + $username = $env:UserName, + [Parameter(Mandatory=$false,Position=1)] + $profile = "default" + ) + + # This is the Alkami process + if (!$username.EndsWith("-cli")) { + $username = "$username-cli" + } + + Write-Host "Attempting to configure credentials for $username" + + $credentialsPath = "~/.aws/credentials" + + $resolvedCredentialsPath = (Resolve-Path -Path $credentialsPath -ErrorAction SilentlyContinue) + + if (($null -eq $resolvedCredentialsPath) -or !(Test-Path $resolvedCredentialsPath)) { + Write-Warning "Could not find the path for $credentialsPath" + Write-Warning "Please ensure the credentials file already exists." + Write-Warning "If you need a sample file please visit https://confluence.alkami.com/display/SECURITY/AWS+CLI+MFA" + } + + $existingCredentialsFile = (Get-Content $resolvedCredentialsPath) + + $haveFoundLineProfile = $false + $haveFoundLineKeyId = $false + $haveFoundLineSecretKey = $false + $existingKeyId = $null + + foreach($line in $existingCredentialsFile) { + if ($haveFoundLineProfile -and $haveFoundLineKeyId -and $haveFoundLineSecretKey) { + break + } + if ($line.Trim() -eq "[$profile]") { + $haveFoundLineProfile = $true + continue + } + if ($haveFoundLineProfile) { + if ($line -match "aws_access_key_id") { + if (!$haveFoundLineKeyId) { + $haveFoundLineKeyId = $true + $existingKeyId = ($line -split "=")[1].Trim() + continue + } + } + if ($line -match "aws_secret_access_key") { + if (!$haveFoundLineSecretKey) { + $haveFoundLineSecretKey = $true + continue + } + } + } + } + + if (!$haveFoundLineProfile) { + throw "could not find the specified profile parameter [$profile] in the file" + } + + if ($null -eq $existingKeyId) { + Write-Warning "There was no valid key found for the file at ~/.aws/credentials" + Write-Warning "While the magic string could be inserted, it is better to just update in place." + Write-Warning "Please ensure the file contains a key/pair entry for aws_access_key_id and aws_secret_access_key" + } + + $newIdentityRaw = (aws iam create-access-key --user-name $username --no-verify-ssl --profile $profile) + $newIdentity = ConvertFrom-Json ($newIdentityRaw | Out-String) + + if (($null -eq $newIdentity.AccessKey.AccessKeyId) -or ($null -eq $newIdentity.AccessKey.SecretAccessKey)) { + throw "Did not get a valid aws response back. oh bother.`r`n$newIdentityRaw" + } + + $newlines = @() + $haveFoundLineProfile = $false + + foreach($line in $existingCredentialsFile) { + if ($line.Trim() -eq "[$profile]") { + $haveFoundLineProfile = $true + $newlines += $line + continue + } + if ($line.Trim().StartsWith("[") -and -not ($line.Trim() -eq "[$profile]")) { + $haveFoundLineProfile = $false + } + if ($haveFoundLineProfile -and $line -match "aws_access_key_id") { + $newlines += "aws_access_key_id = $($newIdentity.AccessKey.AccessKeyId)" + continue + } + if ($haveFoundLineProfile -and $line -match "aws_secret_access_key") { + $newlines += "aws_secret_access_key = $($newIdentity.AccessKey.SecretAccessKey)" + continue + } + $newlines += $line + } + + Write-Host "" + Write-Host "about to delete the following key (in case this breaks, you have this output line)" + Write-Warning "aws iam delete-access-key --access-key-id $existingKeyId --user-name $username --no-verify-ssl --profile $profile" + Write-Host "" + + aws iam delete-access-key --access-key-id $existingKeyId --user-name $username --no-verify-ssl --profile $profile + + + Set-Content -Value $newlines -Path $credentialsPath + + Write-Host "all done" +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Update-AlkamiModules.ps1 b/Modules/Cole.PowerShell.Developer/Public/Update-AlkamiModules.ps1 new file mode 100644 index 0000000..f9678e1 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Update-AlkamiModules.ps1 @@ -0,0 +1,74 @@ +function Update-AlkamiModules { +<# +.SYNOPSIS + This function is a thin shim of how to update the modules. This is not a great way to do this. +#> + # TODO ~ Make it better + [CmdletBinding()] + [OutputType([void])] + param( + [switch]$IncludeDevops, + [switch]$IncludeInstallers, + [switch]$IncludeThirdParty, + [switch]$All + ) + if ($All) { + $IncludeDevops = $true + $IncludeInstallers = $true + $IncludeThirdParty = $true + } + + $knownModuleList = @( + 'Alkami.PowerShell.Common' + 'Alkami.PowerShell.AD' + 'Alkami.PowerShell.Configuration' + 'Alkami.PowerShell.Services' + 'Alkami.PowerShell.Database' + 'Alkami.PowerShell.ServerManagement' + 'Alkami.PowerShell.Choco' + 'Alkami.PowerShell.ServiceFabric' + 'Alkami.PowerShell.IIS' + ) + if ($IncludeDevops) { + $knownModuleList += @( + 'Alkami.Ops.Common' + 'Alkami.Ops.Certificates' + 'Alkami.Ops.SecretServer' + 'Alkami.DevOps.Common' + 'Alkami.DevOps.Certificates' + 'Alkami.DevOps.Installation' + 'Alkami.DevOps.Operations' + 'Alkami.DevOps.SqlReports' + 'Alkami.DevOps.Inventory' + 'Alkami.DevOps.Validations' + ) + } + if ($IncludeInstallers) { + $knownModuleList += @( + 'Alkami.Installer.Widget' + 'Alkami.Installer.Provider' + 'Alkami.Installer.WebExtension' + 'Alkami.Installer.WebApplication' + 'Alkami.Installer.Services' + 'Alkami.Installer.WebSite' + 'Alkami.Installer.LegacyUtility' + 'Alkami.Installer.Migration' + 'Alkami.SRE.MigrationUtility' + ) + } + if ($IncludeThirdParty) { + $knownModuleList += @( + 'AWSPowerShell' + 'PSReadLine' + 'powershell-yaml' + ) + } + + choco upgrade ($knownModuleList -join ';') -fy + + Get-Module Alkami* | Remove-Module -Force + if ($IncludeThirdParty) { + Get-Module AWS* | Remove-Module -Force + } + Get-Module Cole* | Remove-Module -Force +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Update-LastModifiedTimestamp.ps1 b/Modules/Cole.PowerShell.Developer/Public/Update-LastModifiedTimestamp.ps1 new file mode 100644 index 0000000..72e53fe --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Update-LastModifiedTimestamp.ps1 @@ -0,0 +1,66 @@ +function Update-LastModifiedTimestamp { +<# +.SYNOPSIS + This file replicates the familiar bash "touch" command, and is aliased as such. + If the filename present does not exist, it will create the file specified. + If the file already exists, it will update the last-modified time on the file to the current time. + +.PARAMETER Path + [string] The appropriate path + +.PARAMETER Timestamp + [System.DateTime] The specific time to set. Defaults to Now. +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Path, + [Alias('T')] + [System.DateTime]$Timestamp = [System.DateTime]::Now + ) + + if (!(Test-Path -Path $Path)) { + New-File -Path $Path + } + + (Get-Item -Path $Path).LastWriteTime = $Timestamp +} + +Set-Alias -Name touch -Value Update-LastModifiedTimestamp + +<# +GNU coreutils touch man page: + +Usage: touch [OPTION]... FILE... +Update the access and modification times of each FILE to the current time. + +A FILE argument that does not exist is created empty, unless -c or -h +is supplied. + +A FILE argument string of - is handled specially and causes touch to +change the times of the file associated with standard output. + +Mandatory arguments to long options are mandatory for short options too. + -a change only the access time + -c, --no-create do not create any files + -d, --date=STRING parse STRING and use it instead of current time + -f (ignored) + -h, --no-dereference affect each symbolic link instead of any referenced + file (useful only on systems that can change the + timestamps of a symlink) + -m change only the modification time + -r, --reference=FILE use this file's times instead of current time + -t STAMP use [[CC]YY]MMDDhhmm[.ss] instead of current time + --time=WORD change the specified time: + WORD is access, atime, or use: equivalent to -a + WORD is modify or mtime: equivalent to -m + --help display this help and exit + --version output version information and exit + +Note that the -d and -t options accept different time-date formats. + +GNU coreutils online help: +Full documentation +or available locally via: info '(coreutils) touch invocation' +#> \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Update-PowerShellModuleVersion.ps1 b/Modules/Cole.PowerShell.Developer/Public/Update-PowerShellModuleVersion.ps1 new file mode 100644 index 0000000..a8e3d14 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Update-PowerShellModuleVersion.ps1 @@ -0,0 +1,54 @@ +function Update-PowerShellModuleVersion { +<# +.SYNOPSIS + The goal of this function is to prevent needing to remember to update the module version + I can just run this function and magic occurs. + That's the dream anyways +#> + $startingFolder = (Get-Location) + $folder = (Get-Item (Get-Location).Path) + while ($null -eq (Get-ChildItem $folder Modules)) { + $folder = (Get-Item $folder.Parent.FullName) + } + $rootFolderPath = $folder.FullName + Set-Location $rootFolderPath + + $filesModified = (git status --short) + + $foundModules = @() + + foreach($file in $filesModified) { + # get the module name for the affected file + $path = $file.Substring(3) + if ($path -match "Modules/") { + $moduleName = ($path -split '/')[1] + if ($foundModules -notcontains $moduleName) { + $foundModules += $moduleName + } + } + } + + foreach($foundModule in $foundModules) { + $moduleFragment = ("Modules/{0}/{0}.psd1" -f $foundModule) + $moduleFullPath = (Join-Path $rootFolderPath $moduleFragment) + $modified = (((git diff --staged $moduleFragment).Where({$_.StartsWith("+ ")}) -match "ModuleVersion").Count -gt 0) -or (((git diff $moduleFragment).Where({$_.StartsWith("+ ")}) -match "ModuleVersion").Count -gt 0) + + if (!$modified) { + Write-Host "gonna modify the version number for $moduleFragment" + $lines = Get-Content -Path $moduleFullPath + $newLines = @() + foreach ($line in $lines) { + if ($line -match 'ModuleVersion') { + $lineSplits = $line.Split('=') + $lineLead = $lineSplits[0].TrimEnd() + $version = [System.Version]$lineSplits[1].Trim().Replace("'","").Replace('"','') + $version = New-Object System.Version $version.Major, $version.Minor, ($version.Build + 1) + $line = $lineLead + " = '" + $version + "'" + } + $newLines += $line + } + Set-Content -Path $moduleFullPath -Value $newLines + } + } + Set-Location $startingFolder +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Use-ProductionJira.ps1 b/Modules/Cole.PowerShell.Developer/Public/Use-ProductionJira.ps1 new file mode 100644 index 0000000..9d2e002 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Use-ProductionJira.ps1 @@ -0,0 +1,8 @@ +function Use-ProductionJira { + [CmdletBinding()] + param() + + Set-EnvironmentVariable -Name "JIRA_BEARERTOKEN" -StoreName Process -Value (Get-EnvironmentVariable -Name "JIRA_BEARERTOKEN_PROD" -StoreName User) + + Set-EnvironmentVariable -Name JIRA_URL -Value "https://jira.alkami.com/" -StoreName Process +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Use-Staging2Jira.ps1 b/Modules/Cole.PowerShell.Developer/Public/Use-Staging2Jira.ps1 new file mode 100644 index 0000000..7c4b9d7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Use-Staging2Jira.ps1 @@ -0,0 +1,8 @@ +function Use-Staging2Jira { + [CmdletBinding()] + param() + + Set-EnvironmentVariable -Name "JIRA_BEARERTOKEN" -StoreName Process -Value (Get-EnvironmentVariable -Name "JIRA_BEARERTOKEN_STAGING2" -StoreName User) + + Set-EnvironmentVariable -Name JIRA_URL -Value "https://staging2.orb.alkamitech.com/" -StoreName Process +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Use-StagingJira.ps1 b/Modules/Cole.PowerShell.Developer/Public/Use-StagingJira.ps1 new file mode 100644 index 0000000..78cefa4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Use-StagingJira.ps1 @@ -0,0 +1,8 @@ +function Use-StagingJira { + [CmdletBinding()] + param() + + Set-EnvironmentVariable -Name "JIRA_BEARERTOKEN" -StoreName Process -Value (Get-EnvironmentVariable -Name "JIRA_BEARERTOKEN_STAGING" -StoreName User) + + Set-EnvironmentVariable -Name JIRA_URL -Value "https://staging.jira.corp.alkami.net/" -StoreName Process +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Use-TerraformVersion.ps1 b/Modules/Cole.PowerShell.Developer/Public/Use-TerraformVersion.ps1 new file mode 100644 index 0000000..76315da --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Use-TerraformVersion.ps1 @@ -0,0 +1,96 @@ +function Use-TerraformVersion { +<# +.SYNOPSIS + This function will ensure that the version of Terraform in your runtime is the version you want to run. + If you already have a version of terraform downloaded it will just use that version. If not it will download the version to your profile. + This function will ensure the version of terraform you have selected is in the path. + +.PARAMETER Version + The version of terraform to use. Supplying a garbage value will print the list of available values. + +.PARAMETER UseLatestVersion + Use the latest version. +#> + param ( + [Parameter(Mandatory = $false)] + [string]$Version, + [switch]$UseLatestVersion + ) + + $terraformExe = "terraform.exe" + + # Get the version list to verify what you can work with + + $baseUrl = "https://releases.hashicorp.com/" + + $releaseDocument = (Invoke-WebRequest -Uri (Join-UrlComponents -BaseUrl $baseUrl -Path "terraform/") -UseBasicParsing -Method Get) + + $links = $releaseDocument.get_links() + + $latestVersion = $null + $versions = @{} + foreach ($link in $links) { + $text = ($link.outerHtml -split '>' -split '<')[2] + $url = $link.href + $versionSplits = ($text -split '_' -split '-') + $packageVersion = $versionSplits[1] + if (($null -ne $url) -and ($url.IndexOf('terraform') -gt -1) -and ($versionSplits.Count -eq 2)) { + $entry = @{ url = (Join-UrlComponents -BaseUrl $baseUrl -Path $url,"terraform_$($packageVersion)_windows_amd64.zip"); text = $text; version = $packageVersion; } + if ($null -eq $latestVersion) { + $latestVersion = $entry + } + $versions[$packageVersion] = $entry + } + } + + if ($UseLatestVersion) { + $Version = $latestVersion.version + } + + $targetVersion = $versions[$Version] + + if ($null -eq $targetVersion) { + Write-Warning "The specified version [$Version] does not exist in the lookup list." + Write-Host "The available versions are:" + + Show-ListAsTable ($versions.Keys | Sort-Object -Descending) + + throw "The specified version [$Version] does not exist in the lookup list." + } + + $targetPath = (Expand-Path -Path "~/.terraform/$Version") + $terraformPath = (Join-Path $targetPath $terraformExe) + $downloadPath = (Join-Path $targetPath "terraform.zip") + if (!(Test-Path $targetPath)) { + (New-Directory -Path $targetPath) | Out-Null + } + + if (!(Test-Path $terraformPath)) { + Write-Host "Downloading version [$($Version)] from [$(targetVersion.url)] to [$downloadPath]" + + (Invoke-WebRequest -Uri $targetVersion.url -OutFile $downloadPath -Method Get) + Expand-Archive -Path $downloadPath -DestinationPath $targetPath + } + + # Assert that the file exists + if (!(Test-Path $terraformPath)) { + throw "Something has gone wrong, and there is no $terraformExe @ [$terraformPath]" + } + + # now we have a folder that contains our exe file, let's set the path to reference the one we want + $existingLocation = (Get-Command -Name $terraformExe -ErrorAction SilentlyContinue) + $folderToRemove = $null + if ($null -ne $existingLocation) { + # the terraform.exe path is already on our %PATH% so we want to remove it + # Find the folder from the path + $folderToRemove = (Split-Path -Path $existingLocation.Path -Parent) + } + + Write-Host "Ensuring the correct path is set at the machine (removed if set), user and process levels so you can use the right version right away" + + if (![string]::IsNullOrWhiteSpace($folderToRemove)) { + Set-PathVariable -StoreName Machine -Remove $folderToRemove + } + Set-PathVariable -StoreName User -Prepend $targetPath -Remove $folderToRemove + Set-PathVariable -StoreName 'Process' -Prepend $targetPath -Remove $folderToRemove +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-ErrorObject.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-ErrorObject.ps1 new file mode 100644 index 0000000..78872d4 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-ErrorObject.ps1 @@ -0,0 +1,92 @@ +function Write-ErrorObject { +<# +.SYNOPSIS + Write the error object as a well formatted object so it's easier to read and see what happened + +.PARAMETER ErrorItem + The error item to write out. Typically used as $_ | Write-ErrorObject or Write-ErrorObject -ErrorItem $PSItem + +.OUTPUTS + Returns no values. Writes to Write-Host + +.EXAMPLE +Usage: +try { + throw 'something happened' +} catch { + Write-ErrorObject $PSItem +} +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)] + [Alias('ErrorRecord')] + [Alias('Error')] + [System.Management.Automation.ErrorRecord]$ErrorItem, + [switch]$ReturnExtraProperties + ) + + if ($null -eq $ErrorItem) { + $ErrorItem = $Error[0] + } + + $newLine = [System.Environment]::NewLine + + $categorySB = New-Object System.Text.StringBuilder + $errorDetailsSB = New-Object System.Text.StringBuilder + $shortMessageSB = New-Object System.Text.StringBuilder + $exceptionSB = New-Object System.Text.StringBuilder + $stackTraceSB = New-Object System.Text.StringBuilder + + $categoryInfo = $ErrorItem.CategoryInfo + $exception = $ErrorItem.Exception + $invocationInfo = $ErrorItem.InvocationInfo + $errorDetails = $ErrorItem.ErrorDetails + $scriptProperties = @{ ErrorItem = @{}; InvocationInfo = @{}; } + if ($null -ne $ErrorItem.TargetObject) { + $scriptProperties.ErrorItem["TargetObject"] = $ErrorItem.TargetObject + } + + $shortMessageSB.AppendLine("").AppendLine((Format-BoldText "Error Report")).AppendLine("$($ErrorItem.FullyQualifiedErrorId) => $($ErrorItem.ToString())") | Out-Null + $StackTraceSB.AppendLine((Format-UnderlineText (Format-BoldText "Found StackTraces"))).AppendLine($ErrorItem.ScriptStackTrace).AppendLine("===========================") | Out-Null + + if ($null -ne $categoryInfo) { + $categorySB.AppendLine("There was a " + $categoryInfo.Reason + " due to a " + $categoryInfo.Category.ToString() + " when trying to " + $categoryInfo.Activity) | Out-Null + } + if ($null -ne $errorDetails) { + $errorDetailsSB.AppendLine("$(Format-BoldText "Message:") [$($errorDetails.Message)] $(Format-BoldText "RecommendedAction:") [$($errorDetails.RecommendedAction)] $(Format-BoldText "TextLookupError:") [$($errorDetails.TextLookupError)]") | Out-Null + } + if ($null -ne $invocationInfo) { + $shortMessageSB.AppendLine((Format-UnderlineText $invocationInfo.PositionMessage)) | Out-Null + $scriptProperties.InvocationInfo["PSScriptRoot"] = $invocationInfo.PSScriptRoot + $scriptProperties.InvocationInfo["PSCommandPath"] = $invocationInfo.PSCommandPath + if ($null -ne $invocationInfo.BoundParameters) { + $scriptProperties.InvocationInfo["BoundParameters"] = $invocationInfo.BoundParameters + } + if ($null -ne $invocationInfo.UnboundArguments) { + $scriptProperties.InvocationInfo["UnboundArguments"] = $invocationInfo.UnboundArguments + } + } + if ($null -ne $exception) { + $shortMessageSB, $exceptionSB, $stackTraceSB, $scriptProperties = (Write-ExceptionToStringBuilder -Exception $exception -ShortMessageSB $shortMessageSB -ExceptionSB $exceptionSB -StackTraceSB $stackTraceSB -SelectedProperties $scriptProperties) + } + + # Write the interesting data now + $shortMessage = $shortMessageSB.ToString() + $errorDetails = $errorDetailsSB.ToString() + $stackTraces = $stackTraceSB.ToString() + $exceptions = $exceptionSB.ToString() + Write-Host $shortMessage + Write-Host $errorDetails + Write-Host $stackTraces + Write-Host $exceptions + + if ($ReturnExtraProperties) { + return $scriptProperties + } else { + if ($scriptProperties.PSObject.Properties['Keys'].Value.Count -gt 0) { + Write-information "$newLine$($newLine)An additional inspectable property item can be returned for this ErrorRecord in the future by using the flag -ReturnExtraProperties" + } + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-ExceptionToStringBuilder.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-ExceptionToStringBuilder.ps1 new file mode 100644 index 0000000..6ff27d0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-ExceptionToStringBuilder.ps1 @@ -0,0 +1,175 @@ +function Write-ExceptionToStringBuilder { +<# +.SYNOPSIS + This function should be used by Write-ErrorObject to convert the inner exception object(s) to a form that can be used for printing out. + +.PARAMETER Exception + The exception being processed. Can be an InnerException to a parent object. + +.PARAMETER ShortMessageSB + This stringbuilder is used to highlight selected data for the given exception + +.PARAMETER ExceptionSB + This stringbuilder is used to append data for the given exception. + +.PARAMETER StackTraceSB + This stringbuilder is used to append stack trace data for the given exception. + +.PARAMETER SelectedProperties + This object contains selected properties that may be worth investigating + +.OUTPUTS + This function returns a dereferenceable array of the ShortMessage stringbuilder, the Exception stringbuilder, the StackTrace stringbuilder, and the dictionary of selected properties. +#> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [System.Exception]$Exception, + [System.Text.StringBuilder]$ShortMessageSB, + [System.Text.StringBuilder]$ExceptionSB, + [System.Text.StringBuilder]$StackTraceSB, + [int]$InnerExceptionDepth, + [Object]$SelectedProperties + ) + if ($null -eq $SelectedProperties) { + $SelectedProperties = @{} + } + if ($null -eq $ShortMessageSB) { + $ShortMessageSB = New-Object System.Text.StringBuilder + } + if ($null -eq $ExceptionSB) { + $ExceptionSB = New-Object System.Text.StringBuilder + } + if ($null -eq $StackTraceSB) { + $StackTraceSB = New-Object System.Text.StringBuilder + } + + $selectedPropertiesNewKey = "Exception" + if ($InnerExceptionDepth -gt 0) { + $selctedPropertiesNewKey = "InnerException #$InnerExceptionDepth" + } + $selectedPropertiesNewObject = @{} + + $padLeftSize = ($InnerExceptionDepth * 2) + $paddingSpaces = "$(''.PadLeft($padLeftSize))" + + if ($null -ne $exception) { + if ($null -ne $exception.Reason) { + Write-Host "recursing exception.reason" + $ShortMessageSB, $ExceptionSB, $StackTraceSB, $SelectedProperties = (Write-ExceptionToStringBuilder -Exception $exception.Reason -InnerExceptionDepth ($InnerExceptionDepth + 1) -ShortMessageSB $ShortMessageSB -ExceptionSB $ExceptionSB -StackTraceSB $StackTraceSB -SelectedProperties $scriptProperties) + } + if ($null -ne $exception.InnerException) { + $ShortMessageSB, $ExceptionSB, $StackTraceSB, $SelectedProperties = (Write-ExceptionToStringBuilder -Exception $exception.InnerException -InnerExceptionDepth ($InnerExceptionDepth + 1) -ShortMessageSB $ShortMessageSB -ExceptionSB $ExceptionSB -StackTraceSB $StackTraceSB -SelectedProperties $scriptProperties) + } + if ($exception.WasThrownFromThrowStatement) { + $ShortMessageSB.Insert(0,"[This error was thrown via throw]") + } + $ExceptionSB.Append($paddingSpaces).AppendLine("Exception of type $($exception.GetType().FullName) thrown") | Out-Null + $ExceptionSB.Append($paddingSpaces).AppendLine("Message: $($exception.Message)") | Out-Null + if (![string]::IsNullOrWhiteSpace($exception.StackTrace)) { + if ($InnerExceptionDepth -eq 0) { + $StackTraceSB.AppendLine("---------Base Exception Stacktrace:") | Out-Null + } else { + $StackTraceSB.AppendLine("---------Inner (depth: $InnerExceptionDepth) Stacktrace:") | Out-Null + } + $StackTraceSB.AppendLine($exception.StackTrace) | Out-Null + } + + $stringRecords = 'HelpLink','ItemName','ObjectName','ErrorId','ParamName','CommandName','ParameterName','Source','HelpTopic','RequiresShellPath','TypeName','RedirectLocation','Error','Label','AssemblyName','RequiresShellId','TransportMessage','HelpCategory' + $simpleRecords = 'HResult','TypeSpecified','ErrorCode','CallDepth','RequiresPSVersion','WasThrownFromThrowStatement','ParameterType' + $complexRecords = @( + <#System.Object#>'ActualValue' + <#System.Object#>'Argument' + <#System.Management.Automation.PSObject#>'SerializedRemoteException' + <#System.Management.Automation.InvocationInfo#>'CommandInvocation' + <#System.Management.Automation.ErrorRecord#>'ErrorRecord' + <#System.Management.Automation.PSObject#>'SerializedRemoteInvocationInfo' + <#System.Management.Automation.ProviderInvocationException#>'ProviderInvocationException' + <#System.Management.Automation.Language.ScriptExtent#>'DisplayScriptPosition' + <#System.Management.Automation.SessionStateCategory#>'SessionStateCategory' + <#System.Reflection.MethodBase#>'TargetSite' + <#System.Management.Automation.ProviderInfo#>'ProviderInfo' + ) + $complexArrayRecords = @( + <#System.Collections.ObjectModel.ReadOnlyCollection`1[[System.Management.Automation.ProviderInfo, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]#>'PossibleMatches' + <#System.Collections.ObjectModel.ReadOnlyCollection`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]#>'MissingPSSnapIns' + <#System.Management.Automation.PSDataCollection`1[[System.Management.Automation.ErrorRecord, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]#>'ErrorRecords' + ) + + $ExceptionSB.AppendLine("$($paddingSpaces)Exception Object Data:") | Out-Null + foreach ($propertyName in $stringRecords) { + if (![string]::IsNullOrWhiteSpace($exception.$propertyName)) { + $ExceptionSB.AppendLine("$paddingSpaces$propertyName : $($exception.$propertyName)") | Out-Null + } + } + + foreach ($propertyName in $simpleRecords) { + if (($null -ne $exception.$propertyName) -and ($false -ne $exception.$propertyName)) { + $ExceptionSB.AppendLine("$paddingSpaces$propertyName : $(($exception.$propertyName).ToString())") | Out-Null + } + } + foreach ($propertyName in $complexRecords) { +<# + try { + if ($null -ne $exception.$propertyName) { + $value = ConvertTo-Json $exception.$propertyName -Depth 10 + $ExceptionSB.AppendLine("$paddingSpaces$propertyName : $value") + } + } catch { + Write-Warning "Error while attempting to transform error object. This code is untested in some places because the exceptions haven't been seen before." +#> + if ($null -ne $exception.$propertyName) { + Write-information "The value with key [$propertyName] has been attached to the complex data return object for evaluation." + $selectedPropertiesNewObject[$propertyName] = $exception.$propertyName + } +<# + # Error members were captured with this: + # $b = [System.Management.Automation.ParameterBindingException].Assembly.GetExportedTypes().Where({[System.Exception].IsAssignableFrom($_)}) + # $b.GetProperties().Name | Sort-Object -Unique | Set-Clipboard + # $c = $b.GetProperties() + # $d = @{};foreach ($property in $c) { if ($null -eq $d[$property.Name]) { $d[$property.Name] = $property } } + # foreach ($property in $c) { if ($d[$property.Name].PropertyType -ne $property.PropertyType) { Write-Host $property.Name + " " + $property.PropertyType + " " + $d[$property.Name].PropertyType } } + # $e = @();foreach ($key in $d.PSObject.Properties["Keys"].Value) { $value = $d[$key]; if ($null -ne $value) { if ($value.PropertyType.FullName -eq 'System.String') { $e += "if (![string]::IsNullOrWhiteSpace(`$exception.$key)) { `n `$ExceptionSB.Append(`$paddingSpaces)`n #$($value.PropertyType.FullName)`n `$ExceptionSB.AppendLine(`"$key : `$(`$exception.$Key)`")`n}"} else {$e += "if (`$null -eq `$exception.$key) { `n `$ExceptionSB.Append(`$paddingSpaces)`n #$($value.PropertyType.FullName)`n `$ExceptionSB.AppendLine(`"$key : `$(`$exception.$Key)`")`n}"} } }; $e | Set-Clipboard + # This let me get the names of the System.Management.Automation.*Exception properties + # I did not write full translators for each item, so if you need those, you will need to expand this section + Write-Host $_.Exception.Message + Write-Host $_.ToString() + } + } +#> + + foreach ($propertyName in $complexRecords) { +<# + try { + if ($null -ne $exception.$propertyName) { + $value = ConvertTo-Json $exception.$propertyName -Depth 10 + $ExceptionSB.AppendLine("$paddingSpaces$propertyName : $value") + } + } catch { + Write-Warning "Error while attempting to transform error object. This code is untested in some places because the exceptions haven't been seen before." +#> + if ($null -ne $exception.$propertyName) { + Write-information "The value with key [$propertyName] has been attached to the complex data return object for evaluation." + $selectedPropertiesNewObject[$propertyName] = $exception.$propertyName + } +<# + # Error members were captured with this: + # $b = [System.Management.Automation.ParameterBindingException].Assembly.GetExportedTypes().Where({[System.Exception].IsAssignableFrom($_)}) + # $b.GetProperties().Name | Sort-Object -Unique | Set-Clipboard + # $c = $b.GetProperties() + # $d = @{};foreach ($property in $c) { if ($null -eq $d[$property.Name]) { $d[$property.Name] = $property } } + # foreach ($property in $c) { if ($d[$property.Name].PropertyType -ne $property.PropertyType) { Write-Host $property.Name + " " + $property.PropertyType + " " + $d[$property.Name].PropertyType } } + # $e = @();foreach ($key in $d.PSObject.Properties["Keys"].Value) { $value = $d[$key]; if ($null -ne $value) { if ($value.PropertyType.FullName -eq 'System.String') { $e += "if (![string]::IsNullOrWhiteSpace(`$exception.$key)) { `n `$ExceptionSB.Append(`$paddingSpaces)`n #$($value.PropertyType.FullName)`n `$ExceptionSB.AppendLine(`"$key : `$(`$exception.$Key)`")`n}"} else {$e += "if (`$null -eq `$exception.$key) { `n `$ExceptionSB.Append(`$paddingSpaces)`n #$($value.PropertyType.FullName)`n `$ExceptionSB.AppendLine(`"$key : `$(`$exception.$Key)`")`n}"} } }; $e | Set-Clipboard + # This let me get the names of the System.Management.Automation.*Exception properties + # I did not write full translators for each item, so if you need those, you will need to expand this section + Write-Host $_.Exception.Message + Write-Host $_.ToString() +#> + } + } + } + + $SelectedProperties.$selectedPropertiesNewKey = $selectedPropertiesNewObject + + return @($ShortMessageSB, $ExceptionSB, $StackTraceSB, $SelectedProperties) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-JsonArrayNameValuePair.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-JsonArrayNameValuePair.ps1 new file mode 100644 index 0000000..d3a6978 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-JsonArrayNameValuePair.ps1 @@ -0,0 +1,45 @@ +function Write-JsonArrayNameValuePair { + param ( + $JsonName, + $JsonValue, + $Depth = 0, + [switch]$OrderedKeys + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + $quoteString = '"' + $commaString = "," + + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name,Key,Id + } + + $stringBuilder = New-Object System.Text.StringBuilder + $stringBuilder.Append($shortSpacesString) | Out-Null + + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append($quoteString).Append($JsonName).Append($quoteString).Append(" : ") | Out-Null + } + $stringBuilder.AppendLine("[") | Out-Null + + foreach ($iter in $JsonValue) { + if ($iter.GetType().Name -match 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean') { + # Write it without quotes + $stringBuilder.Append($spacesString).Append($iter).AppendLine($commaString) | Out-Null + } elseif ($iter -is [string]) { + # Write it with quotes + $stringBuilder.Append($spacesString).Append($quoteString).Append($iter).Append($quoteString).AppendLine($commaString) | Out-Null + } else { + # Must be a complex object, but without a name, so write the value + $evaluatedValue = (Write-JsonObject -JsonName $null -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine($evaluatedValue) | Out-Null + } + } + $stringBuilder.Append($shortSpacesString) | Out-Null + $stringBuilder.Append("]") | Out-Null + $stringBuilder.Append($commaString) | Out-Null + + $result = ($stringBuilder.ToString()) + + return $result +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveArrayPair.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveArrayPair.ps1 new file mode 100644 index 0000000..8175eb6 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveArrayPair.ps1 @@ -0,0 +1,25 @@ +function Write-JsonNamePrimitiveArrayPair { +<# +.SYNOPSIS + Used to write the value for string pairs to a Json object +#> + param ( + $JsonName, + $JsonValue, + $Depth = 0, + [switch]$OrderedKeys + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + $quoteString = '"' + $commaString = "," + + $stringBuilder = (New-Object System.Text.StringBuilder) + $stringBuilder.Append($shortSpacesString).Append('"{0}" : [' -f $JsonName) | Out-Null + foreach ($iter in $JsonValue) { + $stringBuilder.Append($spacesString).Append($iter).Append($commaString) | Out-Null + } + $stringBuilder.Append($shortSpacesString).Append('],') | Out-Null + + return $stringBuilder.ToString() +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveValuePair.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveValuePair.ps1 new file mode 100644 index 0000000..2e4f616 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNamePrimitiveValuePair.ps1 @@ -0,0 +1,16 @@ +function Write-JsonNamePrimitiveValuePair { +<# +.SYNOPSIS + Used to write the value for primitive pairs to a Json object +#> + param ( + $JsonName, + $JsonValue, + $Depth = 0, + [switch]$OrderedKeys + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + return ('{0}"{1}" : {2},' -f $shortSpacesString, $JsonName, $JsonValue.ToString().ToLower()) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-JsonNameStringValuePair.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNameStringValuePair.ps1 new file mode 100644 index 0000000..8f55925 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-JsonNameStringValuePair.ps1 @@ -0,0 +1,16 @@ +function Write-JsonNameStringValuePair { +<# +.SYNOPSIS + Used to write the value for string pairs to a Json object +#> + param ( + $JsonName, + $JsonValue, + $Depth = 0, + [switch]$OrderedKeys + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + return ('{0}"{1}" : "{2}",' -f $shortSpacesString, $JsonName, $JsonValue) +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-JsonObject.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-JsonObject.ps1 new file mode 100644 index 0000000..7c9abbc --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-JsonObject.ps1 @@ -0,0 +1,64 @@ +function Write-JsonObject { + param ( + $JsonName, + $JsonValue, + $Depth = 0, + [switch]$OrderedKeys + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + $quoteString = '"' + $commaString = "," + $wasSet = $false + + $stringBuilder = (New-Object System.Text.StringBuilder) + $stringBuilder.Append($shortSpacesString) | Out-Null + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null + } + $stringBuilder.AppendLine("{") | Out-Null + if (!(Test-IsCollectionNullOrEmpty $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}))) { + $JsonValue = $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}) + } + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name, Key, Id + } + $keys = $JsonValue.Keys + if (Test-IsCollectionNullOrEmpty $keys) { + $keys = $JsonValue.Name + } + foreach ($key in $keys) { + $iter = $JsonValue[$key] + if ($null -eq $iter) { + $posit = $JsonValue.Where({$_.Name -eq $key}) + if ($null -ne $posit) { + $iter = $posit.Value + } + } + if ($null -eq $iter) { + $evaluatedValue = (Write-JsonNameStringValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine($evaluatedValue) | Out-Null + } else { + if ($iter -ceq "False") { $iter = $false } + if ($iter -ceq "True") { $iter = $true } + if ($iter.GetType().Name -match 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean') { + $evaluatedValue = (Write-JsonNamePrimitiveValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine($evaluatedValue) | Out-Null + } elseif ($iter -is [string]) { + $evaluatedValue = (Write-JsonNameStringValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine($evaluatedValue) | Out-Null + } elseif ($iter.GetType().IsArray) { + # values are an array, so let's write the array values + $evaluatedValue = (Write-JsonArrayNameValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine( $evaluatedValue ) | Out-Null + } else { + # It was not an array, or a primitive, so it must be _another_ object? + $evaluatedValue = (Write-JsonObject -JsonName $key -JsonValue $iter -Depth ($Depth + 1) -OrderedKeys:$OrderedKeys) + $stringBuilder.AppendLine($evaluatedValue) | Out-Null + } + } + } + $stringBuilder.Append($shortSpacesString).Append("}").Append($commaString) | Out-Null + + return $stringBuilder.ToString() +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJson.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJson.ps1 new file mode 100644 index 0000000..56ecf8f --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJson.ps1 @@ -0,0 +1,189 @@ +function Write-OrderedJson { +<# +.SYNOPSIS + Given a PSCustomObject, write it as pretty indented json WITH ORDERING for consistency + +.PARAMETER JsonObject + An object that should be written as json. Should be a PSCustomObject or Hashtable + +.PARAMETER InputPath + A path to a given file to reduce need to import file contents prior to running + +.PARAMETER NoKeyReorder + [switch] Should avoid reordering keys + +.PARAMETER TargetPath + A filesystem path for output +#> + [CmdletBinding(DefaultParameterSetName = 'JsonObject')] + [OutputType([string])] + param ( + # Setting to mandatory false because it can be null and that's ok, we just return null + [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = 'JsonObject')] + $JsonObject, + [Parameter(Mandatory = $true, ParameterSetName = 'InputPath')] + [Alias('Input')] + [Alias('Source')] + [Alias('OriginalFile')] + $Path, + [switch]$NoKeyReorder, + [Parameter(Mandatory = $false)] + [Alias('Output')] + [Alias('Destination')] + $TargetPath + ) + + if ($PSCmdlet.ParameterSetName -eq 'InputPath') { + $JsonObject = (Get-Content -Path $Path -Raw) + } + + if ($JsonObject -is [string]) { + $JsonObject = (ConvertFrom-Json $JsonObject) + } elseif ($JsonObject -is [Array]) { + $JsonObject = (ConvertFrom-Json ([string]::Join('',$JsonObject))) + } + +#region innerFunctions + $stringBuilder = New-Object System.Text.StringBuilder + $quoteString = '"' + $commaString = "," + $OrderedKeys = !$NoKeyReorder + $primitiveMatch = 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean' + + function Get-JsonStringLeadsByDepth { + param ( + $Depth = 0 + ) + + $spacesString = [string]::new(" ", ($Depth + 1) * 2) + $shortSpacesString = "" + if ($Depth -gt 0) { + $shortSpacesString = [string]::new(" ", $Depth * 2) + } + + return ($spacesString, $shortSpacesString) + } + + function Build-JsonArrayNameValuePair { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name,Key,Id + } + + $stringBuilder.Append($shortSpacesString) | Out-Null + + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null + } + $stringBuilder.AppendLine("[") | Out-Null + + if ($OrderedKeys) { + if (($JsonValue[0].GetType().Name -match $primitiveMatch) -or ($JsonValue[0] -is [string])) { + $JsonValue = $JsonValue | Sort-Object + } + } + + foreach ($iter in $JsonValue) { + if ($iter.GetType().Name -match $primitiveMatch) { + # Write it without quotes + $stringBuilder.AppendLine("$spacesString$iter$commaString") | Out-Null + } elseif ($iter -is [string]) { + # Write it with quotes + $stringBuilder.AppendLine("$spacesString$quoteString$iter$quoteString$commaString") | Out-Null + } else { + # Must be a complex object, but without a name, so write the value + Build-JsonObject -JsonName $null -JsonValue $iter -Depth ($Depth + 1) + } + } + $stringBuilder.AppendLine("$shortSpacesString]$commaString") | Out-Null + } + + function Build-JsonObject { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + $stringBuilder.Append($shortSpacesString) | Out-Null + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $stringBuilder.Append("$quoteString$JsonName$quoteString : ") | Out-Null + } + $stringBuilder.AppendLine("{") | Out-Null + if (!(Test-IsCollectionNullOrEmpty $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}))) { + $JsonValue = $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}) + } + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name, Key, Id + } + $keys = $JsonValue.Keys + if (Test-IsCollectionNullOrEmpty $keys) { + $keys = $JsonValue.Name + } + foreach ($key in $keys) { + $iter = $JsonValue[$key] + if ($null -eq $iter) { + $posit = $JsonValue.Where({$_.Name -eq $key}) + if ($null -ne $posit) { + $iter = $posit.Value + } + } + if ($null -eq $iter) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : null$commaString") | Out-Null + } else { + if ($iter -ceq "False") { $iter = $false } + if ($iter -ceq "True") { $iter = $true } + if ($iter.GetType().Name -match $primitiveMatch) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $($iter.ToString().ToLower())$commaString") | Out-Null + } elseif ($iter -is [string]) { + $stringBuilder.AppendLine("$spacesString$quoteString$key$quoteString : $quoteString$iter$quoteString$commaString") | Out-Null + } elseif ($iter.GetType().IsArray) { + # values are an array, so let's write the array values + Build-JsonArrayNameValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } else { + # It was not an array, or a primitive, so it must be _another_ object? + Build-JsonObject -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } + } + } + $stringBuilder.AppendLine("$shortSpacesString}$commaString") | Out-Null + } +#endregion innerFunctions + +#region primitives should just be written as-is + if ($JsonObject -is [string]) { + return "$quoteString$JsonObject$quoteString" + } elseif ($JsonObject -is [bool]) { + return $JsonObject.ToString().ToLower() + } elseif ($JsonObject.GetType().Name -match $primitiveMatch) { + return $JsonObject.ToString() +#endregion primitives should just be written as-is + } elseif ($JsonObject.GetType().IsArray) { + Build-JsonArrayNameValuePair -JsonName $null -JsonValue $JsonObject + } else { + Build-JsonObject -JsonName $null -JsonValue $JsonObject + } + + # postprocess to convert this string: ",(\r?\n?\s*?[\]\}])" to this string "$1" (mind the escaping tho) + # That string says "any comma followed by any newline character(s) and any number of spaces, followed by a closing brace (indicating the end of an array or object) should remove the comma but retain the rest of the string" + # This removes trailing commas in arrays and object notations + $regex = New-Object System.Text.RegularExpressions.Regex(",(\r?\n?\s*?[\]\}])") + $builtString = $stringBuilder.ToString() + $builtString = $regex.Replace($builtString, "`$1") + $builtString = $builtString.TrimEnd().TrimEnd(",") + + if ([string]::IsNullOrWhiteSpace($TargetPath)) { + return $builtString + } else { + Set-Content -Path $TargetPath -Value $builtString -Force | Out-Null + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJsonByArray.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJsonByArray.ps1 new file mode 100644 index 0000000..d81cb9d --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-OrderedJsonByArray.ps1 @@ -0,0 +1,163 @@ +function Write-OrderedJsonByArray { +<# +.SYNOPSIS + Given a PSCustomObject, write it as pretty indented json WITH ORDERING for consistency + +.PARAMETER JsonObject + An object that should be written as json. Should be a PSCustomObject or Hashtable + +.PARAMETER NoKeyReorder + [switch] Should avoid reordering keys + +.PARAMETER Path + A filesystem path +#> + [CmdletBinding()] + [OutputType([string])] + param ( + # Setting to mandatory false because it can be null and that's ok, we just return null + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + $JsonObject, + [switch]$NoKeyReorder, + [Parameter(Mandatory = $false)] + $Path + ) + +#region innerFunctions + $script:fullText = @() + $quoteString = '"' + $commaString = "," + $OrderedKeys = !$NoKeyReorder + $primitiveMatch = 'byte|short|int32|long|sbyte|ushort|uint32|ulong|float|double|decimal|boolean' + + function Get-JsonStringLeadsByDepth { + param ( + $Depth = 0 + ) + + $spacesString = [string]::new(" ", ($Depth + 1) * 2) + $shortSpacesString = "" + if ($Depth -gt 0) { + $shortSpacesString = [string]::new(" ", $Depth * 2) + } + + return ($spacesString, $shortSpacesString) + } + + function Build-JsonArrayNameValuePair { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name,Key,Id + } + + $header = "" + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $header = "$quoteString$JsonName$quoteString : " + } + $header = "$shortSpacesString$header[" + $script:fullText += $header + + foreach ($iter in $JsonValue) { + if ($iter.GetType().Name -match $primitiveMatch) { + # Write it without quotes + $script:fullText += "$spacesString$iter$commaString" + } elseif ($iter -is [string]) { + # Write it with quotes + $script:fullText += "$spacesString$quoteString$iter$quoteString$commaString" + } else { + # Must be a complex object, but without a name, so write the value + Build-JsonObject -JsonName $null -JsonValue $iter -Depth ($Depth + 1) + } + } + $script:fullText += "$shortSpacesString]$commaString" + } + + function Build-JsonObject { + param ( + $JsonName, + $JsonValue, + $Depth = 0 + ) + + $spacesString, $shortSpacesString = (Get-JsonStringLeadsByDepth -Depth $Depth) + + $header = "" + if (![string]::IsNullOrWhiteSpace($JsonName)) { + $header = "$quoteString$JsonName$quoteString : " + } + $header = "$shortSpacesString$header{" + $script:fullText += $header + + if (!(Test-IsCollectionNullOrEmpty $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}))) { + $JsonValue = $JsonValue.PSObject.Properties.Where({$_.MemberType -eq 'NoteProperty'}) + } + if ($OrderedKeys) { + $JsonValue = $JsonValue | Sort-Object -Property Name, Key, Id + } + $keys = $JsonValue.Keys + if (Test-IsCollectionNullOrEmpty $keys) { + $keys = $JsonValue.Name + } + foreach ($key in $keys) { + $iter = $JsonValue[$key] + if ($null -eq $iter) { + $posit = $JsonValue.Where({$_.Name -eq $key}) + if ($null -ne $posit) { + $iter = $posit.Value + } + } + if ($null -eq $iter) { + $script:fullText += "$spacesString$quoteString$key$quoteString : null$commaString" + } else { + if ($iter -ceq "False") { $iter = $false } + if ($iter -ceq "True") { $iter = $true } + if ($iter.GetType().Name -match $primitiveMatch) { + $script:fullText += "$spacesString$quoteString$key$quoteString : $($iter.ToString().ToLower())$commaString" + } elseif ($iter -is [string]) { + $script:fullText += "$spacesString$quoteString$key$quoteString : $quoteString$iter$quoteString$commaString" + } elseif ($iter.GetType().IsArray) { + # values are an array, so let's write the array values + Build-JsonArrayNameValuePair -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } else { + # It was not an array, or a primitive, so it must be _another_ object? + Build-JsonObject -JsonName $key -JsonValue $iter -Depth ($Depth + 1) + } + } + } + $script:fullText += "$shortSpacesString}$commaString" + } +#endregion innerFunctions + + if ($JsonObject -is [string]) { + return $JsonObject + } elseif ($JsonObject -is [bool]) { + return $JsonObject.ToString() + } elseif ($JsonObject.GetType().Name -match $primitiveMatch) { + return $JsonObject.ToString() + } elseif ($JsonObject.GetType().IsArray) { + (Build-JsonArrayNameValuePair -JsonName $null -JsonValue $JsonObject -Depth 0) + } else { + (Build-JsonObject -JsonName $null -JsonValue $JsonObject -Depth 0) + } + + # postprocess to convert this string: ",(\r?\n?\s*?[\]\}])" to this string "$1" (mind the escaping tho) + # That string says "any comma followed by any newline character(s) and any number of spaces, followed by a closing brace (indicating the end of an array or object) should remove the comma but retain the rest of the string" + # This removes trailing commas in arrays and object notations + $regex = New-Object System.Text.RegularExpressions.Regex(",(\r?\n?\s*?[\]\}])") + $builtString = [string]::Join([System.Environment]::NewLine,$script:fullText) + $builtString = $regex.Replace($builtString, "`$1") + $builtString = $builtString.TrimEnd().TrimEnd(",") + + if ([string]::IsNullOrWhiteSpace($Path)) { + return $builtString + } else { + Set-Content -Path $Path -Value $builtString -Force | Out-Null + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-ProgressHelper.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-ProgressHelper.ps1 new file mode 100644 index 0000000..5015197 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-ProgressHelper.ps1 @@ -0,0 +1,54 @@ +function Write-ProgressHelper { + param ( + [int]$StepNumber = 1, + [string]$Status = "Progress", + [string]$Activity, + [string]$CurrentOperation, + [int]$PercentComplete = 0 + ) + + $activityId = 0 + $parentId = 0 + $callstackFunctions = (Get-CallstackParentFunctionNames) + # Don't worry about this function or the one we just called to get the stack, or the parent of this one, we handle that next + for($i = 0; $i -lt ($callstackFunctions.Count - 1); $i++) { + # this will always be one less than our current depth + $function = $callstackFunctions[$i] + if ((Get-FunctionWriteProgressHelperCalls $function) -gt 0) { + $parentId = $activityId + $activityId++ + } + } + + $PercentNotSpecified = $false + if ($PercentComplete -eq 0) { + $PercentNotSpecified = $true + $stepCounter = (Get-FunctionWriteProgressHelperCalls (Get-GrandParentFunctionName)) + $PercentComplete = (($StepNumber / $stepCounter) * 100) + } + + $paramSplat = @{ + Id = $activityId + Status = $Status + PercentComplete = $PercentComplete + } + + if (![string]::IsNullOrWhiteSpace($Activity)) { + $paramSplat["Activity"] = $Activity + } + + if (![string]::IsNullOrWhiteSpace($CurrentOperation)) { + $paramSplat["CurrentOperation"] = $CurrentOperation + } + + if ($parentId -gt 0) { + $paramSplat["ParentId"] = $parentId + } + + Write-Progress @paramSplat + + if ($PercentNotSpecified) { + return $StepNumber++ + } +} + diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-TeamCity.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-TeamCity.ps1 new file mode 100644 index 0000000..793f30b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-TeamCity.ps1 @@ -0,0 +1,63 @@ +function Write-TeamCity { +<# +.SYNOPSIS + Used to write messages for Team-City + +.DESCRIPTION + Will sanitize (escape) the inputs for TeamCity messages if running on TeamCity + +.PARAMETER Message + The special message to write to the TeamCity logs + +.PARAMETER Status + Attach a status to a message. Warning and Error have special significance. + Error is set automatically when you supply an ErrorDetails message. + Default is Normal. + +.PARAMETER ErrorDetails + Can be a StackTrace or more specific details about where/how the error occurred. + +.EXAMPLE +Write-TeamCity -Message "This is a warning message" -Status Warning + +.EXAMPLE +Write-TeamCity -Message "oh no! [this is a bug]" -ErrorDetails "this path: [$path] failed" + +#> + [CmdletBinding(DefaultParameterSetName = 'Message')] + [OutputType([void])] + param( + [Parameter(Mandatory = $true, ParameterSetName = "Message")] + [Parameter(Mandatory = $true, ParameterSetName = "Error")] + [string]$Message, + [Parameter(Mandatory = $false, ParameterSetName = "Message")] + [ValidateSet('Normal','Warning')] + [string]$Status = 'Normal', + [Parameter(Mandatory = $true, ParameterSetName = "Error")] + [string]$ErrorDetails + ) + + if (Test-IsTeamCityProcess) { + $sanitizedErrorDetails = "" + + if ($PSCmdlet.ParameterSetName -eq 'Error') { + $Status = 'Error' + $sanitizedErrorDetails = ConvertTo-SafeTeamCityMessage -InputText $ErrorDetails + $sanitizedErrorDetails = "errorDetails='$sanitizedErrorDetails'" + } + + $sanitizedMessage = ConvertTo-SafeTeamCityMessage -InputText $Message + Write-Host "##teamcity[message text='$sanitizedMessage' $sanitizedErrorDetails status='$($Status.ToUpper())']" + } else { + if ($PSCmdlet.ParameterSetName -eq 'Error') { + Write-Error -Message $Message -ErrorAction Continue + Write-Error -Message $ErrorDetails + } else { + if ($Status -eq 'Warning') { + Write-Warning -Message $Message + } else { + Write-Host $Message + } + } + } +} diff --git a/Modules/Cole.PowerShell.Developer/Public/Write-TeamCityBuildProblem.ps1 b/Modules/Cole.PowerShell.Developer/Public/Write-TeamCityBuildProblem.ps1 new file mode 100644 index 0000000..7c395b7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Public/Write-TeamCityBuildProblem.ps1 @@ -0,0 +1,52 @@ +function Write-TeamCityBuildProblem { +<# +.SYNOPSIS + Writes a build problem. This will halt the build by the end of the build-step. Some build-steps or dependencies may continue depending on their configuration. + +.PARAMETER Description + The message to use in the UI to display that an error occurred + +.PARAMETER Identity + The identity for the error message. + This must be no longer than 60 characters. + This must be a valid Java ID. + This command will trim to 60 characters with a warning if the length is too long. + This may cause you issues if you do not take care to maintain this length. + This will replace spaces and periods in the identity with underscores so it is a proper java identifier, and strip other non-alphanumeric characters. + +.EXAMPLE + Write-TeamCityBuildProblem -Description "This will halt [the build]" -Identity "hahano" +#> + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $true)] + [string]$Description, + [Parameter(Mandatory = $true)] + [string]$Identity + ) + + $logLead = Get-LogLeadName + + # Strip bad things + # Keep all alphanumerics => A-Za-z0-9 are range operators + # Underscores are valid Java ID characters + # Keep spaces and periods to convert them to underscores + $Identity = $Identity -replace '[^a-zA-Z0-9 _.]','' + $Identity = $Identity -replace '[^a-zA-Z0-9]','_' + + # Trim the length + if ($Identity.Length -gt 60) { + Write-Warning "$logLead : Input string (once sanitized) is still longer than 60 characters length [$($Identity.Length)] value [$Identity], trimming to 60" + $Identity = $Identity.Substring(0,60) + } + + $sanitizedDescription = ConvertTo-SafeTeamCityMessage -InputText $Description + + if (Test-IsTeamCityProcess) { + # No need to sanitize the identity, we just stripped it to underscores and alphanumerics + Write-Host "##teamcity[buildProblem description='$sanitizedDescription' identity='$Identity']" + } else { + Write-Error "$logLead : $Identity : $sanitizedDescription" + } +} diff --git a/Modules/Cole.PowerShell.Developer/Scratch/2022-11-28.txt b/Modules/Cole.PowerShell.Developer/Scratch/2022-11-28.txt new file mode 100644 index 0000000..bd7692b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/2022-11-28.txt @@ -0,0 +1,30 @@ +$groupname = (Get-ADPrincipalGroupMembership -Identity (Get-ADComputer -Identity ($env:computername))).Where({ $_.Name -match 'GMSA' }) +$grantAccountsDiskReadAccess = (Get-ADServiceAccount -Filter * -Properties PrincipalsAllowedToRetrieveManagedPassword).Where({ $_.PrincipalsAllowedToRetrieveManagedPassword -eq $groupname }).Name + + +servers that got turned on for deploys, why do we not clean up the tags after we are done??? + + + + +C:\ +Nag\ <-- only happens on primary nag + + +E:\ +ORB\ +Nag\ <-- deployed but not registered + + + +Test-IsPrimaryNag { + return (Test-Path C:\Nag) +} + + +Ask from cody to move the nag config into a structured file so we can be able to recover in case we lose the drive, which has happened in the past + +We should be able to reapply this to a new server in the pod on demand (assuming someone ensured there is no active nag there) + + + diff --git a/Modules/Cole.PowerShell.Developer/Scratch/Compare-File.bak.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/Compare-File.bak.ps1 new file mode 100644 index 0000000..9ff4ee2 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/Compare-File.bak.ps1 @@ -0,0 +1,133 @@ +function Compare-File { +<# +.SYNOPSIS + Compares two files, displaying differences in a manner similar to traditional console-based diff utilities. +#> + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $CompareFrom, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + $CompareTo, + [switch]$Porcelain + ) + + $CompareFrom = (Resolve-Path $CompareFrom).Path + $CompareTo = (Resolve-Path $CompareTo).Path + + $totalFormatWidth = 11 # 5 for numbers, 6 for 2x ' : ' as separators + $screenWidth = Get-ConsoleDisplayWidth + $fixedWidth = ($screenWidth - ($screenWidth % 2) - $totalFormatWidth) / 2 + $fileHeaderPrint = " " + + $fileHeaderFrom = Select-RightSubstringWithPadLeft -String $CompareFrom -Length $fixedWidth + $fileHeaderTo = Select-RightSubstringWithPadLeft -String $CompareTo -Length $fixedWidth + + ## Get the content from each file + $contentFrom = Get-Content -Path $CompareFrom + $contentTo = Get-Content -Path $CompareTo + + ## Compare the two files. Get-Content annotates output objects with + ## a 'ReadCount' property that represents the line number in the file + ## that the text came from. + $comparedLines = Compare-Object -ReferenceObject $contentFrom -DifferenceObject $contentTo -IncludeEqual | Sort-Object { $line.InputObject.ReadCount } + + if ($comparedLines.Count -eq 0) { + if (!$Porcelain) { + "$fileHeaderPrint : $fileHeaderFrom : $fileHeaderTo" + Write-Host "Contents were the same" + } + return + } + + $shortestFileLength = [Math]::Min($contentFrom.Length, $contentTo.Length) + $lineNumberColor = $PSStyle.ForegroundColor.LightCyan + $reset = $PSStyle.Reset + $diffLeftColor = $PSStyle.ForegroundColor.LightRed + $diffRightColor = $PSStyle.ForegroundColor.Green + + $lineNumber = 0 + $fromResults = @{} + $toResults = @{} + $lineNumbers = @() + foreach ($line in $comparedLines) { + $lineNumber = $line.InputObject.ReadCount + if($line.SideIndicator -eq "=>") + { + $lineNumbers += $lineNumber + $lineOperation = "added" + $fromResults[$lineNumber] = $line.InputObject + } + elseif($line.SideIndicator -eq "<=") + { + $lineNumbers += $lineNumber + $lineOperation = "deleted" + $toResults[$lineNumber] = $line.InputObject + } + } + + if ($lineNumbers.Count -gt 0) { + $lineNumbers = $lineNumbers | Sort-Object | Get-Unique + + $groups = Group-Numbers -Values $lineNumbers + $printedLineNumbers = @() + $print = @("$fileHeaderPrint : $fileHeaderFrom : $fileHeaderTo") + + foreach ($group in $groups) { + $first = $group[0] - 2 + if ($first -lt 0) { + $first = 0 + } + $last = $group[-1] + 2 + if ($last -gt $shortestFileLength) { + $last = $shortestFileLength + } + for ($i = $first; $i -lt $last; $i++) { + # We already wrote the line somewhere else (think overlaps) + if ($printedLineNumbers -contains $i) { + continue + } + if ($lineNumbers -contains $i) { + # We need to print this as a difference + $diffLeftColor = $PSStyle.ForegroundColor.LightRed + $diffRightColor = $PSStyle.ForegroundColor.Green + $to = $toResults[$lineNumber] + $from = $fromResults[$lineNumber] + } else { + # This is not a difference line, print it "normally" + $to = $contentTo[$i] + $from = $contentFrom[$i] + $diffLeftColor = $PSStyle.Reset + $diffRightColor = $PSStyle.Reset + } + $lineNumberLead = "$lineNumber".PadLeft(5, ' ') + $from = "$from".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $to = "$to".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $print += "$lineNumberColor$("$i".PadLeft(5))$reset : $diffLeftColor$from$reset : $diffRightColor$to$reset" + $printedLineNumbers += $i + } + } + + if ($print.Count -gt 0) { + $print + } else { + } + } + + $print = @() + foreach ($lineNumber in $lineNumbers) { + $lineNumberLead = "$lineNumber".PadLeft(5, ' ') + $from = $fromResults[$lineNumber] + $from = "$from".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $to = $toResults[$lineNumber] + $to = "$to".PadRight($fixedWidth," ").Substring(0,$fixedWidth) + $print += "$($PSStyle.ForegroundColor.LightCyan)$lineNumberLead$($PSStyle.Reset) : $($PSStyle.ForegroundColor.LightRed)$from$($PSStyle.Reset) : $($PSStyle.ForegroundColor.Green)$to$($PSStyle.Reset)" + } + + if ($print.Count -gt 0) { + "$fileHeaderPrint : $fileHeaderFrom : $fileHeaderTo" + $print + } else { + } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Scratch/Find-LastExecutionTime.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/Find-LastExecutionTime.ps1 new file mode 100644 index 0000000..19ad53a --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/Find-LastExecutionTime.ps1 @@ -0,0 +1,4 @@ +function Find-LastExecutionTime { + $lastCommand = Get-History -Count 1 + if($lastCommand) { ($lastCommand.EndExecutionTime - $lastCommand.StartExecutionTime).TotalMilliseconds ("ms") } +} \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Scratch/Invoke-ParseDemo.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/Invoke-ParseDemo.ps1 new file mode 100644 index 0000000..735aa27 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/Invoke-ParseDemo.ps1 @@ -0,0 +1,51 @@ +clear + +. $PSScriptRoot\Get-HackerText.ps1 +. $PSScriptRoot\Invoke-ScriptedActions.ps1 +. $PSScriptRoot\Register-AlkamiVolume.ps1 +. $PSScriptRoot\Unregister-AlkamiVolume.ps1 +. $PSScriptRoot\Connect-AlkamiVolume.ps1 +. $PSScriptRoot\Disconnect-AlkamiVolume.ps1 +. $PSScriptRoot\scratch.ps1 + +$profileName = 'temp-dev' + +$PromptText = ((prompt | Out-String) -replace "`n", '' -replace "`r", '') +$verbs = (Get-Verb).Verb | Sort-Object + +$text = Get-Content $PSScriptRoot\demo.ps1 + +foreach ($line in $text) { + if ([string]::IsNullOrWhiteSpace($line)) { + Write-Host $PromptText + continue + } + if ($line.Trim()[0] -eq '#') { + Write-Host -NoNewLine $PromptText + Write-Host -ForegroundColor DarkGreen $line.Trim() + continue + } + $maybeVerb = ($line -split '-')[0].Trim() + $actions = Get-HackerText -Textline $line + Invoke-ScriptedActions -Actions $actions + + if ($maybeVerb -in $verbs) { + # execute the line maybe? + try { + Invoke-Expression "$line" + } catch { + Write-Host -ForegroundColor Red -BackgroundColor Black $_ + Write-Host -ForegroundColor Red -BackgroundColor Black $_.Exception + } + } else { + $firstWord = ($line -split ' ')[0] + Write-Host -ForegroundColor Red -BackgroundColor Black @" +$firstWord : The term '$firstWord' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +At line:1 char:1 ++ $line ++ ~~~~ + + CategoryInfo : ObjectNotFound: ($firstWord:String) [], ParentContainsErrorRecordException + + FullyQualifiedErrorId : CommandNotFoundException +"@ + } +} diff --git a/Modules/Cole.PowerShell.Developer/Scratch/SRE-18111.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/SRE-18111.ps1 new file mode 100644 index 0000000..299d8f2 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/SRE-18111.ps1 @@ -0,0 +1,59 @@ +$teamcityFolders = Get-ChildItem -Path C:\CloneAll\DevOps\.teamcity -Directory -Recurse -Depth 4 +$fileMap = @{} + +foreach ($teamcityFolder in $teamcityFolders) { + Set-Location -Path (Split-Path -Path $teamcityFolder.FullName -Parent) + git reset --hard + git checkout main + git checkout master + git pull origin +} + +Set-Location -Path C:\CloneAll +Start-Transcript -Path "z:\temp\SRE-18111.txt" + +foreach ($teamcityFolder in $teamcityFolders) { + $xmlFilesPath = Join-Path -Path $teamcityFolder.FullName -ChildPath "*.xml" + $xmlFiles = Get-ChildItem -Path $xmlFilesPath -File -Recurse + foreach ($xmlFile in $xmlFiles) { + $file = [xml](Get-Content -Path $xmlFile.FullName -Raw) + $uuid = $file.ChildNodes[1].uuid + + if ($null -eq $uuid) { + continue + } + + if ($null -eq $fileMap.$uuid) { + $fileMap.$uuid = @() + } + + $fileMap.$uuid += @{ Name = $file.ChildNodes[1].name; Path = $xmlFile.FullName; FileName = $xmlFile.Name; } + } +} + +foreach ($key in $fileMap.Keys) { + if ($fileMap.$key.Count -gt 1) { + Write-Host "Distinct uuid: $key" + $groups = $fileMap.$key.Name | Group-Object + if (@($groups).Count -eq 1) { + Write-Host "`tFound one distinct name for all of the following files: $($groups[0].Name)" + } else { + Write-Host "`tFound multiple names for the following files" + foreach ($group in $groups) { + Write-Host "`t * $($group.Name)" + } + } + $groups = $fileMap.$key.FileName | Group-Object + if (@($groups).Count -eq 1) { + Write-Host "`tFound one distinct filename for all of the following files: $($groups[0].Name)" + } else { + Write-Host "`tFound multiple filenames for the following files" + foreach ($group in $groups) { + Write-Host "`t * $($group.Name)" + } + } + Write-Host "`t`t$($fileMap.$key.Path -join "`n`t`t")" + } +} + +Stop-Transcript diff --git a/Modules/Cole.PowerShell.Developer/Scratch/add-member usage.txt b/Modules/Cole.PowerShell.Developer/Scratch/add-member usage.txt new file mode 100644 index 0000000..2209af7 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/add-member usage.txt @@ -0,0 +1,307 @@ + +NAME + Add-Member + +SYNOPSIS + Adds custom properties and methods to an instance of a PowerShell object. + + +SYNTAX + Add-Member [-MemberType] {AliasProperty | CodeProperty | Property | NoteProperty | ScriptProperty | Properties | PropertySet | Method | CodeMethod | ScriptMethod | Methods | + ParameterizedProperty | MemberSet | Event | Dynamic | All} [-Name] [[-Value] ] [[-SecondValue] ] [-Force] -InputObject + [-PassThru] [-TypeName ] [] + + Add-Member [-NotePropertyName] [-NotePropertyValue] [-Force] -InputObject [-PassThru] [-TypeName + ] [] + + Add-Member [-NotePropertyMembers] [-Force] -InputObject [-PassThru] [-TypeName ] + [] + + Add-Member -InputObject [-PassThru] [-TypeName ] [] + + +DESCRIPTION + The `Add-Member` cmdlet lets you add members (properties and methods) to an instance of a PowerShell object. For instance, you can add a NoteProperty member that contains a + description of the object or a ScriptMethod member that runs a script to change the object. + + To use `Add-Member`, pipe the object to `Add-Member`, or use the InputObject parameter to specify the object. + + The MemberType parameter indicates the type of member that you want to add. The Name parameter assigns a name to the new member, and the Value parameter sets the value of the + member. + + The properties and methods that you add are added only to the particular instance of the object that you specify. `Add-Member` does not change the object type. To create a + new object type, use the `Add-Type` cmdlet. + + You can also use the `Export-Clixml` cmdlet to save the instance of the object, including the additional members, in a file. Then you can use the `Import-Clixml` cmdlet to + re-create the instance of the object from the information that is stored in the exported file. + + Beginning in Windows PowerShell 3.0, `Add-Member` has new features that make it easier to add note properties to objects. You can use the NotePropertyName and + NotePropertyValue parameters to define a note property or use the NotePropertyMembers parameter, which takes a hash table of note property names and values. + + Also, beginning in Windows PowerShell 3.0, the PassThru parameter, which generates an output object, is needed less frequently. `Add-Member` now adds the new members directly + to the input object of more types. For more information, see the PassThru parameter description. + + +PARAMETERS + -Force + Indicates that this cmdlet adds a new member even the object has a custom member with the same name. You cannot use the Force parameter to replace a standard member of a + type. + + Required? false + Position? named + Default value False + Accept pipeline input? False + Accept wildcard characters? false + + -InputObject + Specifies the object to which the new member is added. Enter a variable that contains the objects, or type a command or expression that gets the objects. + + Required? true + Position? named + Default value None + Accept pipeline input? True (ByValue) + Accept wildcard characters? false + + -MemberType + Specifies the type of the member to add. This parameter is required. The acceptable values for this parameter are: + + - NoteProperty + + - AliasProperty + + - ScriptProperty + + - CodeProperty + + - ScriptMethod + + - CodeMethod + + + For information about these values, see PSMemberTypes Enumeration (/dotnet/api/system.management.automation.psmembertypes)in the PowerShell SDK. + Not all objects have every type of member. If you specify a member type that the object does not have, PowerShell returns an error. + + + Required? true + Position? 0 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -Name + Specifies the name of the member that this cmdlet adds. + + Required? true + Position? 1 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -NotePropertyMembers + Specifies a hash table or ordered dictionary of note property names and values. Type a hash table or dictionary in which the keys are note property names and the values + are note property values. + + For more information about hash tables and ordered dictionaries in PowerShell, see about_Hash_Tables (../Microsoft.PowerShell.Core/About/about_Hash_Tables.md). + + This parameter was introduced in Windows PowerShell 3.0. + + Required? true + Position? 0 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -NotePropertyName + Specifies the note property name. + + Use this parameter with the NotePropertyValue parameter. This parameter is optional. + + This parameter was introduced in Windows PowerShell 3.0. + + Required? true + Position? 0 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -NotePropertyValue + Specifies the note property value. + + Use this parameter with the NotePropertyName parameter. This parameter is optional. + + This parameter was introduced in Windows PowerShell 3.0. + + Required? true + Position? 1 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -PassThru + Returns an object representing the item with which you are working. By default, this cmdlet does not generate any output. + + For most objects, `Add-Member` adds the new members to the input object. However, when the input object is a string, `Add-Member` cannot add the member to the input + object. For these objects, use the PassThru parameter to create an output object. + + In Windows PowerShell 2.0, `Add-Member` added members only to the PSObject wrapper of objects, not to the object. Use the PassThru parameter to create an output object + for any object that has a PSObject wrapper. + + Required? false + Position? named + Default value False + Accept pipeline input? False + Accept wildcard characters? false + + -SecondValue + Specifies optional additional information about AliasProperty , ScriptProperty , CodeProperty , or CodeMethod members. + + If used when adding an AliasProperty , this parameter must be a data type. A conversion to the specified data type is added to the value of the AliasProperty . + + For example, if you add an AliasProperty that provides an alternate name for a string property, you can also specify a SecondValue parameter of System.Int32 to indicate + that the value of that string property should be converted to an integer when accessed by using the corresponding AliasProperty . + + You can use the SecondValue parameter to specify an additional ScriptBlock when adding a ScriptProperty member. The first ScriptBlock , specified in the Value parameter, + is used to get the value of a variable. The second ScriptBlock , specified in the SecondValue parameter, is used to set the value of a variable. + + Required? false + Position? 3 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -Value + Specifies the initial value of the added member. If you add an AliasProperty , CodeProperty , ScriptProperty or CodeMethod member, you can supply optional, additional + information by using the SecondValue parameter. + + Required? false + Position? 2 + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + -TypeName + Specifies a name for the type. + + When the type is a class in the System namespace or a type that has a type accelerator, you can enter the short name of the type. Otherwise, the full type name is + required. This parameter is effective only when the InputObject is a PSObject . + + This parameter was introduced in Windows PowerShell 3.0. + + Required? false + Position? named + Default value None + Accept pipeline input? False + Accept wildcard characters? false + + + This cmdlet supports the common parameters: Verbose, Debug, + ErrorAction, ErrorVariable, WarningAction, WarningVariable, + OutBuffer, PipelineVariable, and OutVariable. For more information, see + about_CommonParameters (https:/go.microsoft.com/fwlink/?LinkID=113216). + +INPUTS + System.Management.Automation.PSObject + You can pipe any object type to this cmdlet. + + +OUTPUTS + None or System.Object + When you use the PassThru parameter, this cmdlet returns the newly-extended object. Otherwise, this cmdlet does not generate any output. + + +NOTES + + + You can add members only to PSObject objects. To determine whether an object is a PSObject object, use the `-is` operator. + + For instance, to test an object stored in the `$obj` variable, type `$obj -is [PSObject]`. + + The names of the MemberType , Name , Value , and SecondValue parameters are optional. If you omit the parameter names, the unnamed parameter values must appear in this + order: MemberType , Name , Value , and SecondValue . + + If you include the parameter names, the parameters can appear in any order. + + You can use the `$this` automatic variable in script blocks that define the values of new properties and methods. The `$this` variable refers to the instance of the + object to which the properties and methods are being added. For more information about the `$this` variable, see about_Automatic_Variables + (../Microsoft.PowerShell.Core/About/about_Automatic_Variables.md). + + --------- Example 1: Add a note property to a PSObject --------- + + $A = Get-ChildItem c:\ps-test\test.txt + $A | Add-Member -NotePropertyName Status -NotePropertyValue Done + $A.Status + + Done + + + -------- Example 2: Add an alias property to a PSObject -------- + + $A = Get-ChildItem C:\Temp\test.txt + $A | Add-Member -MemberType AliasProperty -Name Size -Value Length + $A.Size + + 2394 + + + ----- Example 3: Add a StringUse note property to a string ----- + + $A = "A string" + $A = $A | Add-Member -NotePropertyMembers @{StringUse="Display"} -PassThru + $A.StringUse + + Display + + + ----- Example 4: Add a script method to a FileInfo object ----- + + $A = Get-ChildItem C:\Temp\test.txt + $S = {[math]::Round(($this.Length / 1MB), 2)} + $A | Add-Member -MemberType ScriptMethod -Name "SizeInMB" -Value $S + $A.SizeInMB() + + 0.43 + + + ---- Example 5: Copy all properties of an object to another ---- + + function Copy-Property ($From, $To) + { + $properties = Get-Member -InputObject $From -MemberType Property + foreach ($p in $properties) + { + $To | Add-Member -MemberType NoteProperty -Name $p.Name -Value $From.$($p.Name) -Force + } + } + + + -------------- Example 6: Create a custom object -------------- + + $Asset = New-Object -TypeName PSObject + $d = [ordered]@{Name="Server30";System="Server Core";PSVersion="4.0"} + $Asset | Add-Member -NotePropertyMembers $d -TypeName Asset + $Asset | Get-Member + + TypeName: Asset + + Name MemberType Definition + ---- ---------- ---------- + Equals Method bool Equals(System.Object obj) + GetHashCode Method int GetHashCode() + GetType Method type GetType() + ToString Method string ToString() + Name NoteProperty System.String Name=Server30 + PSVersion NoteProperty System.String PSVersion=4.0 + System NoteProperty System.String System=Server Core + + + +RELATED LINKS + Online Version: https://docs.microsoft.com/powershell/module/microsoft.powershell.utility/add-member?view=powershell-5.1&WT.mc_id=ps-gethelp + Export-Clixml + Get-Member + Import-Clixml + New-Object + about_Automatic_Variables + + + diff --git a/Modules/Cole.PowerShell.Developer/Scratch/apply_windows_updates.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/apply_windows_updates.ps1 new file mode 100644 index 0000000..1aa53a0 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/apply_windows_updates.ps1 @@ -0,0 +1,232 @@ +<# +.SYNOPSIS + Find and apply Windows Updates on remote computers + +.PARAMETER ComputerNames + One or more fully qualified computer names as a string array + +.PARAMETER doRestarts + Restart when completed +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string[]]$ComputerNames, + [switch]$doUpdates, + [switch]$doRestarts +) + +<# +Determine which packages need to be upgraded per server for the individual AWS items + +$bucketResult = (Invoke-RestMethod -Method GET -Uri https://s3.amazonaws.com/ec2-windows-drivers-downloads) +$types = @('NVMe','ENA','AWSPV') +$results = @() +foreach ($key in $bucketResult.ListBucketResult.Contents.Key) { + foreach ($type in $types) { + if ($key.StartsWith($type)) { + $results += @{ Type = ($key -split '/')[0]; Version = ($key -split '/')[1] } + } + } +} +$versionTable = @{} +foreach ($type in $types) { + $version = $results.Where({$_.Version -match '[0-9]+\.[0-9]+\.[0-9]+'}).Where({$_.Type -eq $type}).Version | Sort-Object -Descending | Select-Object -First 1 + Write-Host $version + Write-Host $type + $versionTable.$type = $version +} + +$JobNames = @{ + NVMe = 'AWSNVMe' + ENA = 'AwsEnaNetworkDriver' + AWSPV = 'AWSPVDriver' +} + + +$x = Get-EC2WindowsDriverVersions +$toUpdate = $x.Where({$_.Version -ne $versionTable[$_.Type]}) + +$instances = (Get-CachedInstances -ProfileName temp-prod).Where({$_.Hostname -match '^tea'}) + +foreach ($instance in $instances) { + foreach ($update in $toUpdate) { + if ($instance.Hostname -eq "$($update.ComputerName).fh.local") { + Write-Host "updating" + Invoke-AWSConfigureAWSPackage -JobName $JobNames[$update.Type] -InstanceId $instance.InstanceId -Comment 'SRE-17714' -ProfileName 'temp-prod' -Region $instance.Region + } + } +} +#> + +Write-Host "Updating chrome on all remote machines" +$sbChrome = { + choco upgrade GoogleChrome -y -r +} +Invoke-Command -ComputerName $ComputerNames -ScriptBlock $sbChrome + +#region magic session things for PowerShell +# The COMObjects can not be created if you don't do this first +# I should probably have them get removed after the run, but meh +Write-Host "Ensuring VirtualAccount PSSessionConfiguration exists locally so we can run updates remotely" +if ($null -eq (Get-PSSessionConfiguration -Name 'VirtualAccount' -ErrorAction SilentlyContinue)) { + New-PSSessionConfigurationFile -RunAsVirtualAccount -Path .\VirtualAccount.pssc + # Note this will restart the WinRM service: + Register-PSSessionConfiguration -Name 'VirtualAccount' -Path .\VirtualAccount.pssc -Force +} + +$ensureVirtualAccountSessionManagementExistsScriptBlock = { + Write-Host "Ensuring VirtualAccount PSSessionConfiguration exists so we can run updates remotely on $($env:COMPUTERNAME)" + if ($null -eq (Get-PSSessionConfiguration -Name 'VirtualAccount' -ErrorAction SilentlyContinue)) { + New-PSSessionConfigurationFile -RunAsVirtualAccount -Path .\VirtualAccount.pssc + # Note this will restart the WinRM service: + Register-PSSessionConfiguration -Name 'VirtualAccount' -Path .\VirtualAccount.pssc -Force + } +} +Invoke-Command -ComputerName $ComputerNames -ScriptBlock $ensureVirtualAccountSessionManagementExistsScriptBlock +#endregion magic session things for PowerShell + +# Take inventory, find out if we need to reboot before we continue +$rebootRequiredScriptBlock = { + param ( + [string]$computerName + ) + + $session = New-PSSession -ComputerName $computerName -ConfigurationName 'VirtualAccount' + + $serverScript = { + $UpdateCollection = New-Object -ComObject 'Microsoft.Update.UpdateColl' -Strict + $Searcher = New-Object -ComObject 'Microsoft.Update.Searcher' -Strict + $Session = New-Object -ComObject 'Microsoft.Update.Session' -Strict + $Installer = New-Object -ComObject 'Microsoft.Update.Installer' -Strict + + $Searcher.Search("") | Out-Null + $totalHistoryCount = $Searcher.GetTotalHistoryCount() + + $returnUpdates = @() + $Updates = @($Searcher.Search("IsHidden=0 and IsInstalled=0").Updates) + foreach ($update in $Updates) { + $UpdateCollection.Add($update) | Out-Null + $returnUpdates += New-Object -Type PSObject -Property @{ + BundledUpdates = $update.BundledUpdates + Categories = $update.Categories + CveIDs = $update.CveIDs + Deadline = $update.Deadline + DeltaCompressedContentAvailable = $update.DeltaCompressedContentAvailable + DeltaCompressedContentPreferred = $update.DeltaCompressedContentPreferred + DeploymentAction = $update.DeploymentAction + Description = $update.Description + DownloadPriority = $update.DownloadPriority + EulaAccepted = $update.EulaAccepted + EulaText = $update.EulaText + HandlerID = $update.HandlerID + Identity = $update.Identity + InstallationBehavior = $update.InstallationBehavior + IsBeta = $update.IsBeta + IsDownloaded = $update.IsDownloaded + IsHidden = $update.IsHidden + IsInstalled = $update.IsInstalled + IsMandatory = $update.IsMandatory + IsPresent = $update.IsPresent + IsUninstallable = $update.IsUninstallable + KBArticleIDs = $update.KBArticleIDs + Languages = $update.Languages + LastDeploymentChangeTime = $update.LastDeploymentChangeTime + MoreInfoUrls = $update.MoreInfoUrls + MsrcSeverity = $update.MsrcSeverity + RebootRequired = $update.RebootRequired + RecommendedCpuSpeed = $update.RecommendedCpuSpeed + RecommendedHardDiskSpace = $update.RecommendedHardDiskSpace + RecommendedMemory = $update.RecommendedMemory + ReleaseNotes = $update.ReleaseNotes + SecurityBulletinIDs = $update.SecurityBulletinIDs + SupersededUpdateIDs = $update.SupersededUpdateIDs + SupportUrl = $update.SupportUrl + Title = $update.Title + Type = $update.Type + } + } + + if ($UpdateCollection.Count -gt 0) { + $Downloader = $Session.CreateUpdateDownloader() + $Downloader.Updates = $UpdateCollection + $Downloader.Download() | Out-Null + } + + $Installer.AllowSourcePrompts = $true + $installer.ForceQuiet = $true + $Installer.Updates = $UpdateCollection + $isRebootRequired = $installer.RebootRequiredBeforeInstallation + + return $isRebootRequired, $returnUpdates, $totalHistoryCount + } + $isRebootRequired, $returnUpdates, $totalHistoryCount = Invoke-Command -Session $session -ScriptBlock $serverScript + Exit-PSSession + $retValue = @{ ComputerName = $computerName; IsRebootRequired = $isRebootRequired; Updates = $returnUpdates; TotalHistoryCount = $totalHistoryCount } + + return $retValue +} + +$doInstallsScriptBlock = { + param ( + $computerName + ) + $session = New-PSSession -ComputerName $computerName -ConfigurationName 'VirtualAccount' + $serverScript = { + $UpdateCollection = New-Object -ComObject Microsoft.Update.UpdateColl + $Searcher = New-Object -ComObject Microsoft.Update.Searcher + $Session = New-Object -ComObject Microsoft.Update.Session + $Installer = New-Object -ComObject Microsoft.Update.Installer + + $Updates = @($Searcher.Search("IsHidden=0 and IsInstalled=0").Updates) + foreach ($update in $Updates) { + $UpdateCollection.Add($update) | Out-Null + } + + if ($UpdateCollection.Count -gt 0) { + $Downloader = $Session.CreateUpdateDownloader() + $Downloader.Updates = $UpdateCollection + $Downloader.Download() | Out-Null + } + + $Installer.AllowSourcePrompts = $true + $installer.ForceQuiet = $true + $Installer.Updates = $UpdateCollection + Write-Host "Beginning install" + $result = $Installer.Install() + + if ($result.ResultCode -ne 2) { + Write-Warning "$($env:COMPUTERNAME) ResultCode was not 2, it was $($result.ResultCode)" + } + + Write-Host "$($env:COMPUTERNAME) finished installing, time to reboot?" + } + Invoke-Command -Session $session -ScriptBlock $serverScript + Exit-PSSession + return @{ ComputerName = $computerName; InstallCompleted = $true } +} + +$results = Invoke-Parallel -ScriptBlock $rebootRequiredScriptBlock -objects $ComputerNames -ReturnObjects +Write-Output $results + +# TODO: Reboot before continuing, if the flag above was true +if ($results.IsRebootRequired) { + throw "reboots are required on one or more servers. Please reboot before continuing" +} + +if ($doUpdates) { + $results = Invoke-Parallel -ScriptBlock $doInstallsScriptBlock -objects $ComputerNames -ReturnObjects +} + +if ($doRestarts) { + $computerNames = 'tea31697.fh.local','tea316155.fh.local','tea316208.fh.local','tea316229.fh.local','tea46658.fh.local' + foreach ($computerName in $computerNames) { + if ([string]::IsNullOrWhiteSpace($computerName)) { + Write-Host "Shutting down $computerName" + shutdown -m $computerName -t 0 -r + } + } +} + +Write-Host "finished" \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Scratch/audit radium and nag services.ps1 b/Modules/Cole.PowerShell.Developer/Scratch/audit radium and nag services.ps1 new file mode 100644 index 0000000..a557f17 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/audit radium and nag services.ps1 @@ -0,0 +1,209 @@ +$ProfileName = "temp-prod" + +Get-CachedInstances -Profile $ProfileName -IncludeDR -App + + +$stoppedInstancesOverflowApp = Get-CachedInstances -Profile $ProfileName -IncludeDR -App | Where-Object { ($_.Tags."alk:overflow" -eq $true) -and ($_.CaptureState.Value -eq 'Stopped') } +$runningInstancesOverflowApp = Get-CachedInstances -Profile $ProfileName -IncludeDR -App | Where-Object { ($_.Tags."alk:overflow" -eq $true) -and ($_.CaptureState.Value -eq 'Running') } +$stoppedInstancesNotOverflowApp = Get-CachedInstances -Profile $ProfileName -IncludeDR -App | Where-Object { ($_.Tags."alk:overflow" -ne $true) -and ($_.CaptureState.Value -eq 'Stopped') } +$runningInstancesNotOverflowApp = Get-CachedInstances -Profile $ProfileName -IncludeDR -App | Where-Object { ($_.Tags."alk:overflow" -ne $true) -and ($_.CaptureState.Value -eq 'Running') } + +if (($stoppedInstancesOverflowApp.Count + $stoppedInstancesNotOverflowApp.Count + $runningInstancesOverflowApp.Count + $runningInstancesNotOverflowApp.Count) -ne ((Get-CachedInstances -Profile temp-prod -IncludeDR -App).Count)) { + throw "the server counts don't match" +} + +$runningInstancesNotOverflowApp = $runningInstancesNotOverflowApp.Where({ $_.Hostname -notmatch 'view' }) + +$stoppedOverflowProcessed = Get-Content -Path Z:\stuff\stoppedOverflow.txt | ConvertFrom-Json +$stoppedNotOverflowProcessed = Get-Content -Path Z:\stuff\stoppedNotOverflow.txt | ConvertFrom-Json +$runningInstancesNotOverflowProcessed = Get-Content -Path Z:\stuff\runningNotOverflow.txt | ConvertFrom-Json +$runningInstancesOverflowProcessed = Get-Content -Path Z:\stuff\runningOverflow.txt | ConvertFrom-Json + +$processed = $stoppedOverflowProcessed +$processed += $stoppedNotOverflowProcessed +$processed += $runningInstancesNotOverflowProcessed +$processed += $runningInstancesOverflowProcessed + +$stoppedInstancesOverflowApp = $stoppedInstancesOverflowApp | Where-Object { $_.Hostname -notin $processed} +$runningInstancesOverflowApp = $runningInstancesOverflowApp | Where-Object { $_.Hostname -notin $processed } +$stoppedInstancesNotOverflowApp = $stoppedInstancesNotOverflowApp | Where-Object { $_.Hostname -notin $processed } +$runningInstancesNotOverflowApp = $runningInstancesNotOverflowApp | Where-Object { $_.Hostname -notin $processed } + + +return + +throw 'no' + +Start-Transcript -Path Z:\stuff\transcript.txt -Append + + +# $stoppedInstancesOverflowApp +# $stoppedInstancesNotOverflowApp +# $runningInstancesOverflowApp +# $runningInstancesNotOverflowApp + +# process for offline servers: +# start the server +# Sleep one minute +# Check to see if it is online by Get-Services +# If it is still not online, sleep again till we get services +# Now that it is online, report the nag service name, radium service name, hostname, "not overflow" +# Turn the server back off again + +Write-Host "`n`tStarting overflow app servers`n" +$stoppedInstancesOverflowServices = @() +foreach ($instance in $stoppedInstancesOverflowApp) { + if (Test-StringIsNullOrEmpty $instance.Hostname) { + Write-Warning "$($instance.InstanceID) did not have a hostname - This will confuse restarts" + continue + } + if ($instance.LiveState -ne 'Stopped') { + Write-Warning "$($instance.InstanceID) was not stopped but should have been based on capture state. Will not action this instance." + continue + } + + # start the server + Write-Host "Starting instance [$($instance.InstanceID)]" + Start-EC2Instance -InstanceId $instance.InstanceID -Region $instance.Region -ProfileName $ProfileName + + try { + $testService = $null + while ($null -eq $testService) { + # Sleep one minute + # Check to see if it is online by Get-Services + # If it is still not online, sleep again till we get services + + Write-Host "Sleeping for 60s" + Start-Sleep -Seconds 60 + $testService = Invoke-Command -ComputerName $instance.Hostname -ScriptBlock { (Get-Service)[0] } -ErrorAction SilentlyContinue + } + Write-Host "Got a service back on [$($instance.InstanceID)]" + + # Now that it is online, report the nag service name, radium service name, hostname, "overflow" + $serviceInfo = Invoke-Command -ComputerName $instance.Hostname -ScriptBlock { + return @{ + Hostname = (Get-FullyQualifiedServerName) + NagServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Nag").Name + RadiumServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Radium").Name + Indicator = "stopped overflow" + } + } + $serviceInfo + $stoppedInstancesOverflowServices += $serviceInfo + } catch { + Write-Host $_.Exception.Message + Write-Host "Instance failed on data collection, will proceed to shutdown process" + } + + try { + # Turn the server back off again + Write-Host "Stopping instance [$($instance.InstanceID)]" + Stop-Computer -ComputerName $instance.Hostname + } catch { + try { + Write-Host "Stopping instance [$($instance.InstanceID)]" + Stop-Computer -ComputerName $instance.Hostname + } catch { + Write-Host "Stop-Computer -ComputerName $($instance.Hostname)" + } + } + Write-Host "----------" + Write-Host "" +} +ConvertTo-Json $stoppedInstancesOverflowServices -Depth 10 | Set-Content -Path Z:\stuff\stoppedOverflow.txt + + +Write-Host "`n`tStarting not-overflow app servers`n" + +$stoppedInstancesNotOverflowServices = @() +foreach ($instance in $stoppedInstancesNotOverflowApp) { + if (Test-StringIsNullOrEmpty $instance.Hostname) { + Write-Warning "$($instance.InstanceID) did not have a hostname - This will confuse restarts" + continue + } + if ($instance.LiveState -ne 'Stopped') { + Write-Warning "$($instance.InstanceID) was not stopped but should have been based on capture state. Will not action this instance." + continue + } + + # start the server + Write-Host "Starting instance [$($instance.InstanceID)]" + Start-EC2Instance -InstanceId $instance.InstanceID -Region $instance.Region -ProfileName $ProfileName + + try { + $testService = $null + while ($null -eq $testService) { + # Sleep one minute + # Check to see if it is online by Get-Services + # If it is still not online, sleep again till we get services + + Write-Host "Sleeping for 60s" + Start-Sleep -Seconds 60 + $testService = Invoke-Command -ComputerName $instance.Hostname -ScriptBlock { (Get-Service)[0] } -ErrorAction SilentlyContinue + } + Write-Host "Got a service back on [$($instance.InstanceID)]" + + # Now that it is online, report the nag service name, radium service name, hostname, "overflow" + $serviceInfo = Invoke-Command -ComputerName $instance.Hostname -ScriptBlock { + return @{ + Hostname = (Get-FullyQualifiedServerName) + NagServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Nag").Name + RadiumServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Radium").Name + Indicator = "stopped overflow" + } + } + $serviceInfo + $stoppedInstancesNotOverflowServices += $serviceInfo + } catch { + Write-Host $_.Exception.Message + Write-Host "Instance failed on data collection, will proceed to shutdown process" + } + + try { + # Turn the server back off again + Write-Host "Stopping instance [$($instance.InstanceID)]" + Stop-Computer -ComputerName $instance.Hostname + } catch { + try { + Write-Host "Stopping instance [$($instance.InstanceID)]" + Stop-Computer -ComputerName $instance.Hostname + } catch { + Write-Host "Stop-Computer -ComputerName $($instance.Hostname)" + } + } + Write-Host "----------" + Write-Host "" +} +ConvertTo-Json $stoppedInstancesNotOverflowServices -Depth 10 | Set-Content -Path Z:\stuff\stoppedNotOverflow.txt + +Write-Host "`n`tGathering overflow app servers`n" + +# $runningInstancesOverflowServices = Invoke-Command -ComputerName $runningInstancesOverflowApp.Hostname -ScriptBlock { +# return @{ +# Hostname = (Get-FullyQualifiedServerName) +# NagServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Nag").Name +# RadiumServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Radium").Name +# Indicator = "running overflow" +# } +# } +# ConvertTo-Json $runningInstancesOverflowServices -Depth 10 | Set-Content -Path Z:\stuff\runningOverflow.txt + +# Write-Host "`n`tGathering not-overflow app servers`n" + +# $runningInstancesNonOverflowServices = Invoke-Command -ComputerName $runningInstancesNotOverflowApp.Hostname -ScriptBlock { +# return @{ +# Hostname = (Get-FullyQualifiedServerName) +# NagServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Nag").Name +# RadiumServiceName = (Get-ServiceInfoByCIMFragment "C:\Orb\Radium").Name +# Indicator = "running -not overflow" +# } +# } +# ConvertTo-Json $runningInstancesNonOverflowServices -Depth 10 | Set-Content -Path Z:\stuff\runningNotOverflow.txt + +Stop-Transcript + +$audit = @() +$audit += $stoppedInstancesNotOverflowServices +$audit += $stoppedInstancesOverflowServices +$audit += $runningInstancesNonOverflowServices +$audit += $runningInstancesOverflowServices diff --git a/Modules/Cole.PowerShell.Developer/Scratch/choco install usage.txt b/Modules/Cole.PowerShell.Developer/Scratch/choco install usage.txt new file mode 100644 index 0000000..909f05b --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/choco install usage.txt @@ -0,0 +1,441 @@ +Chocolatey v0.10.15 +Install Command + +Installs a package or a list of packages (sometimes specified as a + packages.config). Some may prefer to use `cinst` as a shortcut for + `choco install`. + +NOTE: 100% compatible with older chocolatey client (0.9.8.32 and below) + with options and switches. Add `-y` for previous behavior with no + prompt. In most cases you can still pass options and switches with one + dash (`-`). For more details, see the command reference (`choco -?`). + +Usage + + choco install [ ] [] + cinst [ ] [] + +NOTE: `all` is a special package keyword that will allow you to install + all packages from a custom feed. Will not work with Chocolatey default + feed. THIS IS NOT YET REIMPLEMENTED. + +NOTE: Any package name ending with .config is considered a + 'packages.config' file. Please see https://bit.ly/packages_config + +NOTE: Chocolatey Pro / Business builds on top of a great open source + experience with quite a few features that enhance the your use of the + community package repository (when using Pro), and really enhance the + Chocolatey experience all around. If you are an organization looking + for a better ROI, look no further than Business - automatic package + creation from installer files, automatic recompile support, runtime + malware protection, private CDN download cache, synchronize with + Programs and Features, etc - https://chocolatey.org/compare. + + +Examples + + choco install sysinternals + choco install notepadplusplus googlechrome atom 7zip + choco install notepadplusplus --force --force-dependencies + choco install notepadplusplus googlechrome atom 7zip -dvfy + choco install git -y --params="'/GitAndUnixToolsOnPath /NoAutoCrlf'" + choco install git -y --params="'/GitAndUnixToolsOnPath /NoAutoCrlf'" --install-arguments="'/DIR=C:\git'" + # Params are package parameters, passed to the package + # Install args are installer arguments, appended to the silentArgs + # in the package for the installer itself + choco install nodejs.install --version 0.10.35 + choco install git -s "'https://somewhere/out/there'" + choco install git -s "'https://somewhere/protected'" -u user -p pass + +Choco can also install directly from a nuspec/nupkg file. This aids in + testing packages: + + choco install + choco install + +Install multiple versions of a package using -m (AllowMultiple versions) + + choco install ruby --version 1.9.3.55100 -my + choco install ruby --version 2.0.0.59800 -my + choco install ruby --version 2.1.5 -my + +What is `-my`? See option bundling in the command reference + (`choco -?`). + +NOTE: All of these will add to PATH variable. We'll be adding a special + option to not allow PATH changes. Until then you will need to manually + go modify Path to just one Ruby and then use something like uru + (https://bitbucket.org/jonforums/uru) or pik + (https://chocolatey.org/packages/pik) to switch between versions. + +NOTE: See scripting in the command reference (`choco -?`) for how to + write proper scripts and integrations. + + +Exit Codes + +Exit codes that normally result from running this command. + +Normal: + - 0: operation was successful, no issues detected + - -1 or 1: an error has occurred + +Package Exit Codes: + - 1641: success, reboot initiated + - 3010: success, reboot required + - other (not listed): likely an error has occurred + +In addition to normal exit codes, packages are allowed to exit + with their own codes when the feature 'usePackageExitCodes' is + turned on. Uninstall command has additional valid exit codes. + Available in v0.9.10+. + +Reboot Exit Codes: + - 350: pending reboot detected, no action has occurred + - 1604: install suspended, incomplete + +In addition to the above exit codes, you may also see reboot exit codes + when the feature 'exitOnRebootDetected' is turned on. It typically requires + the feature 'usePackageExitCodes' to also be turned on to work properly. + Available in v0.10.12+. + +See It In Action + +Chocolatey FOSS install showing tab completion and `refreshenv` (a way + to update environment variables without restarting the shell). + +FOSS install in action: https://raw.githubusercontent.com/wiki/chocolatey/choco/images/gifs/choco_install.gif + +Chocolatey Professional showing private download cache and virus scan + protection. + +Pro install in action: https://raw.githubusercontent.com/wiki/chocolatey/choco/images/gifs/chocopro_install_stopped.gif + +Packages.config + +Alternative to PackageName. This is a list of packages in an xml manifest for Chocolatey to install. This is like the packages.config that NuGet uses except it also adds other options and switches. This can also be the path to the packages.config file if it is not in the current working directory. + +NOTE: The filename is only required to end in .config, the name is not required to be packages.config. + + + + + + + + + + +Alternative Sources + +Available in 0.9.10+. + +Ruby +This specifies the source is Ruby Gems and that we are installing a + gem. If you do not have ruby installed prior to running this command, + the command will install that first. + e.g. `choco install compass -source ruby` + +WebPI +This specifies the source is Web PI (Web Platform Installer) and that + we are installing a WebPI product, such as IISExpress. If you do not + have the Web PI command line installed, it will install that first and + then the product requested. + e.g. `choco install IISExpress --source webpi` + +Cygwin +This specifies the source is Cygwin and that we are installing a cygwin + package, such as bash. If you do not have Cygwin installed, it will + install that first and then the product requested. + e.g. `choco install bash --source cygwin` + +Python +This specifies the source is Python and that we are installing a python + package, such as Sphinx. If you do not have easy_install and Python + installed, it will install those first and then the product requested. + e.g. `choco install sphinx --source python` + +Windows Features +This specifies that the source is a Windows Feature and we should + install via the Deployment Image Servicing and Management tool (DISM) + on the local machine. + e.g. `choco install IIS-WebServerRole --source windowsfeatures` + + +Resources + + * How-To: A complete example of how you can use the PackageParameters argument + when creating a Chocolatey Package can be seen at + https://chocolatey.org/docs/how-to-parse-package-parameters-argument + * One may want to override the default installation directory of a + piece of software. See + https://chocolatey.org/docs/getting-started#overriding-default-install-directory-or-other-advanced-install-concepts. + + +Options and Switches + +NOTE: Options and switches apply to all items passed, so if you are + installing multiple packages, and you use `--version=1.0.0`, it is + going to look for and try to install version 1.0.0 of every package + passed. So please split out multiple package calls when wanting to + pass specific options. + + + -?, --help, -h + Prints out the help menu. + + -d, --debug + Debug - Show debug messaging. + + -v, --verbose + Verbose - Show verbose messaging. Very verbose messaging, avoid using + under normal circumstances. + + --trace + Trace - Show trace messaging. Very, very verbose trace messaging. Avoid + except when needing super low-level .NET Framework debugging. Available + in 0.10.4+. + + --nocolor, --no-color + No Color - Do not show colorization in logging output. This overrides + the feature 'logWithoutColor', set to 'False'. Available in 0.10.9+. + + --acceptlicense, --accept-license + AcceptLicense - Accept license dialogs automatically. Reserved for + future use. + + -y, --yes, --confirm + Confirm all prompts - Chooses affirmative answer instead of prompting. + Implies --accept-license + + -f, --force + Force - force the behavior. Do not use force during normal operation - + it subverts some of the smart behavior for commands. + + --noop, --whatif, --what-if + NoOp / WhatIf - Don't actually do anything. + + -r, --limitoutput, --limit-output + LimitOutput - Limit the output to essential information + + --timeout, --execution-timeout=VALUE + CommandExecutionTimeout (in seconds) - The time to allow a command to + finish before timing out. Overrides the default execution timeout in the + configuration of 2700 seconds. '0' for infinite starting in 0.10.4. + + -c, --cache, --cachelocation, --cache-location=VALUE + CacheLocation - Location for download cache, defaults to %TEMP% or value + in chocolatey.config file. + + --allowunofficial, --allow-unofficial, --allowunofficialbuild, --allow-unofficial-build + AllowUnofficialBuild - When not using the official build you must set + this flag for choco to continue. + + --failstderr, --failonstderr, --fail-on-stderr, --fail-on-standard-error, --fail-on-error-output + FailOnStandardError - Fail on standard error output (stderr), typically + received when running external commands during install providers. This + overrides the feature failOnStandardError. + + --use-system-powershell + UseSystemPowerShell - Execute PowerShell using an external process + instead of the built-in PowerShell host. Should only be used when + internal host is failing. Available in 0.9.10+. + + --no-progress + Do Not Show Progress - Do not show download progress percentages. + Available in 0.10.4+. + + --proxy=VALUE + Proxy Location - Explicit proxy location. Overrides the default proxy + location of ''. Available for config settings in 0.9.9.9+, this CLI + option available in 0.10.4+. + + --proxy-user=VALUE + Proxy User Name - Explicit proxy user (optional). Requires explicity + proxy (`--proxy` or config setting). Overrides the default proxy user of + ''. Available for config settings in 0.9.9.9+, this CLI option available + in 0.10.4+. + + --proxy-password=VALUE + Proxy Password - Explicit proxy password (optional) to be used with + username. Requires explicity proxy (`--proxy` or config setting) and + user name. Overrides the default proxy password (encrypted in settings + if set). Available for config settings in 0.9.9.9+, this CLI option + available in 0.10.4+. + + --proxy-bypass-list=VALUE + ProxyBypassList - Comma separated list of regex locations to bypass on + proxy. Requires explicity proxy (`--proxy` or config setting). Overrides + the default proxy bypass list of ''. Available in 0.10.4+. + + --proxy-bypass-on-local + Proxy Bypass On Local - Bypass proxy for local connections. Requires + explicity proxy (`--proxy` or config setting). Overrides the default + proxy bypass on local setting of 'True'. Available in 0.10.4+. + + --log-file=VALUE + Log File to output to in addition to regular loggers. Available in 0.1- + 0.8+. + + -s, --source=VALUE + Source - The source to find the package(s) to install. Special sources + include: ruby, webpi, cygwin, windowsfeatures, and python. To specify + more than one source, pass it with a semi-colon separating the values (- + e.g. "'source1;source2'"). Defaults to default feeds. + + --version=VALUE + Version - A specific version to install. Defaults to unspecified. + + --pre, --prerelease + Prerelease - Include Prereleases? Defaults to false. + + --x86, --forcex86 + ForceX86 - Force x86 (32bit) installation on 64 bit systems. Defaults to + false. + + --ia, --installargs, --install-args, --installarguments, --install-arguments=VALUE + InstallArguments - Install Arguments to pass to the native installer in + the package. Defaults to unspecified. + + -o, --override, --overrideargs, --overridearguments, --override-arguments + OverrideArguments - Should install arguments be used exclusively without + appending to current package passed arguments? Defaults to false. + + --notsilent, --not-silent + NotSilent - Do not install this silently. Defaults to false. + + --params, --parameters, --pkgparameters, --packageparameters, --package-parameters=VALUE + PackageParameters - Parameters to pass to the package. Defaults to + unspecified. + + --argsglobal, --args-global, --installargsglobal, --install-args-global, --applyargstodependencies, --apply-args-to-dependencies, --apply-install-arguments-to-dependencies + Apply Install Arguments To Dependencies - Should install arguments be + applied to dependent packages? Defaults to false. + + --paramsglobal, --params-global, --packageparametersglobal, --package-parameters-global, --applyparamstodependencies, --apply-params-to-dependencies, --apply-package-parameters-to-dependencies + Apply Package Parameters To Dependencies - Should package parameters be + applied to dependent packages? Defaults to false. + + --allowdowngrade, --allow-downgrade + AllowDowngrade - Should an attempt at downgrading be allowed? Defaults + to false. + + -m, --sxs, --sidebyside, --side-by-side, --allowmultiple, --allow-multiple, --allowmultipleversions, --allow-multiple-versions + AllowMultipleVersions - Should multiple versions of a package be + installed? Defaults to false. + + -i, --ignoredependencies, --ignore-dependencies + IgnoreDependencies - Ignore dependencies when installing package(s). + Defaults to false. + + -x, --forcedependencies, --force-dependencies + ForceDependencies - Force dependencies to be reinstalled when force + installing package(s). Must be used in conjunction with --force. + Defaults to false. + + -n, --skippowershell, --skip-powershell, --skipscripts, --skip-scripts, --skip-automation-scripts + Skip Powershell - Do not run chocolateyInstall.ps1. Defaults to false. + + -u, --user=VALUE + User - used with authenticated feeds. Defaults to empty. + + -p, --password=VALUE + Password - the user's password to the source. Defaults to empty. + + --cert=VALUE + Client certificate - PFX pathname for an x509 authenticated feeds. + Defaults to empty. Available in 0.9.10+. + + --cp, --certpassword=VALUE + Certificate Password - the client certificate's password to the source. + Defaults to empty. Available in 0.9.10+. + + --ignorechecksum, --ignore-checksum, --ignorechecksums, --ignore-checksums + IgnoreChecksums - Ignore checksums provided by the package. Overrides + the default feature 'checksumFiles' set to 'True'. Available in 0.9.9.9+. + + --allowemptychecksum, --allowemptychecksums, --allow-empty-checksums + Allow Empty Checksums - Allow packages to have empty/missing checksums + for downloaded resources from non-secure locations (HTTP, FTP). Use this + switch is not recommended if using sources that download resources from + the internet. Overrides the default feature 'allowEmptyChecksums' set to + 'False'. Available in 0.10.0+. + + --allowemptychecksumsecure, --allowemptychecksumssecure, --allow-empty-checksums-secure + Allow Empty Checksums Secure - Allow packages to have empty checksums + for downloaded resources from secure locations (HTTPS). Overrides the + default feature 'allowEmptyChecksumsSecure' set to 'True'. Available in + 0.10.0+. + + --requirechecksum, --requirechecksums, --require-checksums + Require Checksums - Requires packages to have checksums for downloaded + resources (both non-secure and secure). Overrides the default feature + 'allowEmptyChecksums' set to 'False' and 'allowEmptyChecksumsSecure' set + to 'True'. Available in 0.10.0+. + + --checksum, --downloadchecksum, --download-checksum=VALUE + Download Checksum - a user provided checksum for downloaded resources + for the package. Overrides the package checksum (if it has one). + Defaults to empty. Available in 0.10.0+. + + --checksum64, --checksumx64, --downloadchecksumx64, --download-checksum-x64=VALUE + Download Checksum 64bit - a user provided checksum for 64bit downloaded + resources for the package. Overrides the package 64-bit checksum (if it + has one). Defaults to same as Download Checksum. Available in 0.10.0+. + + --checksumtype, --checksum-type, --downloadchecksumtype, --download-checksum-type=VALUE + Download Checksum Type - a user provided checksum type. Overrides the + package checksum type (if it has one). Used in conjunction with Download + Checksum. Available values are 'md5', 'sha1', 'sha256' or 'sha512'. + Defaults to 'md5'. Available in 0.10.0+. + + --checksumtype64, --checksumtypex64, --checksum-type-x64, --downloadchecksumtypex64, --download-checksum-type-x64=VALUE + Download Checksum Type 64bit - a user provided checksum for 64bit + downloaded resources for the package. Overrides the package 64-bit + checksum (if it has one). Used in conjunction with Download Checksum + 64bit. Available values are 'md5', 'sha1', 'sha256' or 'sha512'. + Defaults to same as Download Checksum Type. Available in 0.10.0+. + + --ignorepackagecodes, --ignorepackageexitcodes, --ignore-package-codes, --ignore-package-exit-codes + IgnorePackageExitCodes - Exit with a 0 for success and 1 for non-succes- + s, no matter what package scripts provide for exit codes. Overrides the + default feature 'usePackageExitCodes' set to 'True'. Available in 0.- + 9.10+. + + --usepackagecodes, --usepackageexitcodes, --use-package-codes, --use-package-exit-codes + UsePackageExitCodes - Package scripts can provide exit codes. Use those + for choco's exit code when non-zero (this value can come from a + dependency package). Chocolatey defines valid exit codes as 0, 1605, + 1614, 1641, 3010. Overrides the default feature 'usePackageExitCodes' + set to 'True'. Available in 0.9.10+. + + --stoponfirstfailure, --stop-on-first-failure, --stop-on-first-package-failure + Stop On First Package Failure - stop running install, upgrade or + uninstall on first package failure instead of continuing with others. + Overrides the default feature 'stopOnFirstPackageFailure' set to 'False- + '. Available in 0.10.4+. + + --exitwhenrebootdetected, --exit-when-reboot-detected + Exit When Reboot Detected - Stop running install, upgrade, or uninstall + when a reboot request is detected. Requires 'usePackageExitCodes' + feature to be turned on. Will exit with either 350 or 1604. Overrides + the default feature 'exitOnRebootDetected' set to 'False'. Available in + 0.10.12+. + + --ignoredetectedreboot, --ignore-detected-reboot + Ignore Detected Reboot - Ignore any detected reboots if found. Overrides + the default feature 'exitOnRebootDetected' set to 'False'. Available in + 0.10.12+. + + --disable-repository-optimizations, --disable-package-repository-optimizations + Disable Package Repository Optimizations - Do not use optimizations for + reducing bandwidth with repository queries during package + install/upgrade/outdated operations. Should not generally be used, + unless a repository needs to support older methods of query. When used, + this makes queries similar to the way they were done in Chocolatey v0.1- + 0.11 and before. Overrides the default feature + 'usePackageRepositoryOptimizations' set to 'True'. Available in 0.10.14+. \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Scratch/choco usage.txt b/Modules/Cole.PowerShell.Developer/Scratch/choco usage.txt new file mode 100644 index 0000000..8826ed8 --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/choco usage.txt @@ -0,0 +1,277 @@ +This is a listing of all of the different things you can pass to choco. + +Commands + + * list - lists remote or local packages + * find - searches remote or local packages (alias for search) + * search - searches remote or local packages (alias for list) + * info - retrieves package information. Shorthand for choco search pkgname --exact --verbose + * install - installs packages from various sources + * pin - suppress upgrades for a package + * outdated - retrieves packages that are outdated. Similar to upgrade all --noop + * upgrade - upgrades packages from various sources + * uninstall - uninstalls a package + * pack - packages up a nuspec to a compiled nupkg + * push - pushes a compiled nupkg + * new - generates files necessary for a chocolatey package from a template + * sources - view and configure default sources (alias for source) + * source - view and configure default sources + * config - Retrieve and configure config file settings + * feature - view and configure choco features + * features - view and configure choco features (alias for feature) + * setapikey - retrieves, saves or deletes an apikey for a particular source (alias for apikey) + * apikey - retrieves, saves or deletes an apikey for a particular source + * unpackself - have chocolatey set itself up + * version - [DEPRECATED] will be removed in v1 - use `choco outdated` or `cup -whatif` instead + * update - [DEPRECATED] RESERVED for future use (you are looking for upgrade, these are not the droids you are looking for) + + +Please run chocolatey with `choco command -help` for specific help on + each command. + +How To Pass Options / Switches + +You can pass options and switches in the following ways: + + * Unless stated otherwise, an option/switch should only be passed one + time. Otherwise you may find weird/non-supported behavior. + * `-`, `/`, or `--` (one character switches should not use `--`) + * **Option Bundling / Bundled Options**: One character switches can be + bundled. e.g. `-d` (debug), `-f` (force), `-v` (verbose), and `-y` + (confirm yes) can be bundled as `-dfvy`. + * NOTE: If `debug` or `verbose` are bundled with local options + (not the global ones above), some logging may not show up until after + the local options are parsed. + * **Use Equals**: You can also include or not include an equals sign + `=` between options and values. + * **Quote Values**: When you need to quote an entire argument, such as + when using spaces, please use a combination of double quotes and + apostrophes (`"'value'"`). In cmd.exe you can just use double quotes + (`"value"`) but in powershell.exe you should use backticks + (`` `"value`" ``) or apostrophes (`'value'`). Using the combination + allows for both shells to work without issue, except for when the next + section applies. + * **Pass quotes in arguments**: When you need to pass quoted values to + to something like a native installer, you are in for a world of fun. In + cmd.exe you must pass it like this: `-ia "/yo=""Spaces spaces"""`. In + PowerShell.exe, you must pass it like this: `-ia '/yo=""Spaces spaces""'`. + No other combination will work. In PowerShell.exe if you are on version + v3+, you can try `--%` before `-ia` to just pass the args through as is, + which means it should not require any special workarounds. + * **Periods in PowerShell**: If you need to pass a period as part of a + value or a path, PowerShell doesn't always handle it well. Please + quote those values using "Quote Values" section above. + * Options and switches apply to all items passed, so if you are + installing multiple packages, and you use `--version=1.0.0`, choco + is going to look for and try to install version 1.0.0 of every + package passed. So please split out multiple package calls when + wanting to pass specific options. + +Scripting / Integration - Best Practices / Style Guide + +When writing scripts, such as PowerShell scripts passing options and +switches, there are some best practices to follow to ensure that you +don't run into issues later. This also applies to integrations that +are calling Chocolatey and parsing output. Chocolatey **uses** +PowerShell, but it is an exe, so it cannot return PowerShell objects. + +Following these practices ensures both readability of your scripts AND +compatibility across different versions and editions of Chocolatey. +Following this guide will ensure your experience is not frustrating +based on choco not receiving things you think you are passing to it. + + * For consistency, always use `choco`, not `choco.exe`. Never use + shortcut commands like `cinst` or `cup`. + * Always have the command as the first argument to `choco. e.g. + `choco install`, where `install` is the command. + * If there is a subcommand, ensure that is the second argument. e.g. + `choco source list`, where `source` is the command and `list` is the + subcommand. + * Typically the subject comes next. If installing packages, the + subject would be the package names, e.g. `choco install pkg1 pkg2`. + * Never use 'nupkg' or point directly to a nupkg file UNLESS using + 'choco push'. Use the source folder instead, e.g. `choco install + --source="'c:\folder\with\package'"` instead of + `choco install DoNotDoThis.1.0.nupkg` or `choco install DoNotDoThis + --source="'c:\folder\with\package\DoNotDoThis.1.0.nupkg'"`. + * Switches and parameters are called simply options. Options come + after the subject. e.g. `choco install pkg1 --debug --verbose`. + * Never use the force option (`--force`/`-f`) in scripts (or really + otherwise as a default mode of use). Force is an override on + Chocolatey behavior. If you are wondering why Chocolatey isn't doing + something like the documentation says it should, it's likely because + you are using force. Stop. + * Always use full option name. If the short option is `-n`, and the + full option is `--name`, use `--name`. The only acceptable short + option for use in scripts is `-y`. Find option names in help docs + online or through `choco -?` /`choco [Command Name] -?`. + * For scripts that are running automated, always use `-y`. Do note + that even with `-y` passed, some things / state issues detected will + temporarily stop for input - the key here is temporarily. They will + continue without requiring any action after the temporary timeout + (typically 30 seconds). + * Full option names are prepended with two dashes, e.g. `--` or + `--debug --verbose --ignore-proxy`. + * When setting a value to an option, always put an equals (`=`) + between the name and the setting, e.g. `--source="'local'"`. + * When setting a value to an option, always surround the value + properly with double quotes bookending apostrophes, e.g. + `--source="'internal_server'"`. + * If you are building PowerShell scripts, you can most likely just + simply use apostrophes surrounding option values, e.g. + `--source='internal_server'`. + * Prefer upgrade to install in scripts. You can't `install` to a newer + version of something, but you can `choco upgrade` which will do both + upgrade or install (unless switched off explicitly). + * If you are sharing the script with others, pass `--source` to be + explicit about where the package is coming from. Use full link and + not source name ('https://chocolatey.org/api/v2' versus + 'chocolatey'). + * If parsing output, you might want to use `--limit-output`/`-r` to + get output in a more machine parseable format. NOTE: Not all + commands handle return of information in an easily digestible + output. + * Use exit codes to determine status. Chocolatey exits with 0 when + everything worked appropriately and other exits codes like 1 when + things error. There are package specific exit codes that are + recommended to be used and reboot indicating exit codes as well. To + check exit code when using PowerShell, immediately call + `$exitCode = $LASTEXITCODE` to get the value choco exited with. + +Here's an example following bad practices (line breaks added for + readability): + + `choco install pkg1 -y -params '/Option:Value /Option2:value with + spaces' --c4b-option 'Yaass' --option-that-is-new 'dude upgrade'` + +Now here is that example written with best practices (again line + breaks added for readability - there are not line continuations + for choco): + + `choco upgrade pkg1 -y --source="'https://chocolatey.org/api/v2'" + --package-parameters="'/Option:Value /Option2:value with spaces'" + --c4b-option="'Yaass'" --option-that-is-new="'dude upgrade'"` + +Note the differences between the two: + * Which is more self-documenting? + * Which will allow for the newest version of something installed or + upgraded to (which allows for more environmental consistency on + packages and versions)? + * Which may throw an error on a badly passed option? + * Which will throw errors on unknown option values? See explanation + below. + +Chocolatey ignores options it doesn't understand, but it can only + ignore option values if they are tied to the option with an + equals sign ('='). Note those last two options in the examples above? + If you roll off of a commercial edition or someone with older version + attempts to run the badly crafted script `--c4b-option 'Yaass' + --option-that-is-new 'dude upgrade'`, they are likely to see errors on + 'Yaass' and 'dude upgrade' because they are not explicitly tied to the + option they are written after. Now compare that to the other script. + Choco will ignore `--c4b-option="'Yaass'"` and + `--option-that-is-new="'dude upgrade'"` as a whole when it doesn't + register the options. This means that your script doesn't error. + +Following these scripting best practices will ensure your scripts work + everywhere they are used and with newer versions of Chocolatey. + + +Default Options and Switches + + -?, --help, -h + Prints out the help menu. + + -d, --debug + Debug - Show debug messaging. + + -v, --verbose + Verbose - Show verbose messaging. Very verbose messaging, avoid using + under normal circumstances. + + --trace + Trace - Show trace messaging. Very, very verbose trace messaging. Avoid + except when needing super low-level .NET Framework debugging. Available + in 0.10.4+. + + --nocolor, --no-color + No Color - Do not show colorization in logging output. This overrides + the feature 'logWithoutColor', set to 'False'. Available in 0.10.9+. + + --acceptlicense, --accept-license + AcceptLicense - Accept license dialogs automatically. Reserved for + future use. + + -y, --yes, --confirm + Confirm all prompts - Chooses affirmative answer instead of prompting. + Implies --accept-license + + -f, --force + Force - force the behavior. Do not use force during normal operation - + it subverts some of the smart behavior for commands. + + --noop, --whatif, --what-if + NoOp / WhatIf - Don't actually do anything. + + -r, --limitoutput, --limit-output + LimitOutput - Limit the output to essential information + + --timeout, --execution-timeout=VALUE + CommandExecutionTimeout (in seconds) - The time to allow a command to + finish before timing out. Overrides the default execution timeout in the + configuration of 2700 seconds. '0' for infinite starting in 0.10.4. + + -c, --cache, --cachelocation, --cache-location=VALUE + CacheLocation - Location for download cache, defaults to %TEMP% or value + in chocolatey.config file. + + --allowunofficial, --allow-unofficial, --allowunofficialbuild, --allow-unofficial-build + AllowUnofficialBuild - When not using the official build you must set + this flag for choco to continue. + + --failstderr, --failonstderr, --fail-on-stderr, --fail-on-standard-error, --fail-on-error-output + FailOnStandardError - Fail on standard error output (stderr), typically + received when running external commands during install providers. This + overrides the feature failOnStandardError. + + --use-system-powershell + UseSystemPowerShell - Execute PowerShell using an external process + instead of the built-in PowerShell host. Should only be used when + internal host is failing. Available in 0.9.10+. + + --no-progress + Do Not Show Progress - Do not show download progress percentages. + Available in 0.10.4+. + + --proxy=VALUE + Proxy Location - Explicit proxy location. Overrides the default proxy + location of ''. Available for config settings in 0.9.9.9+, this CLI + option available in 0.10.4+. + + --proxy-user=VALUE + Proxy User Name - Explicit proxy user (optional). Requires explicity + proxy (`--proxy` or config setting). Overrides the default proxy user of + ''. Available for config settings in 0.9.9.9+, this CLI option available + in 0.10.4+. + + --proxy-password=VALUE + Proxy Password - Explicit proxy password (optional) to be used with + username. Requires explicity proxy (`--proxy` or config setting) and + user name. Overrides the default proxy password (encrypted in settings + if set). Available for config settings in 0.9.9.9+, this CLI option + available in 0.10.4+. + + --proxy-bypass-list=VALUE + ProxyBypassList - Comma separated list of regex locations to bypass on + proxy. Requires explicity proxy (`--proxy` or config setting). Overrides + the default proxy bypass list of ''. Available in 0.10.4+. + + --proxy-bypass-on-local + Proxy Bypass On Local - Bypass proxy for local connections. Requires + explicity proxy (`--proxy` or config setting). Overrides the default + proxy bypass on local setting of 'True'. Available in 0.10.4+. + + --log-file=VALUE + Log File to output to in addition to regular loggers. Available in 0.1- + 0.8+. +Chocolatey v0.10.15 \ No newline at end of file diff --git a/Modules/Cole.PowerShell.Developer/Scratch/cloc usage.txt b/Modules/Cole.PowerShell.Developer/Scratch/cloc usage.txt new file mode 100644 index 0000000..8848dcd --- /dev/null +++ b/Modules/Cole.PowerShell.Developer/Scratch/cloc usage.txt @@ -0,0 +1,405 @@ + +Usage: cloc.exe [options] | | + + Count, or compute differences of, physical lines of source code in the + given files (may be archives such as compressed tarballs or zip files, + or git commit hashes or branch names) and/or recursively below the + given directories. + + Input Options + --extract-with= This option is only needed if cloc is unable + to figure out how to extract the contents of + the input file(s) by itself. + Use to extract binary archive files (e.g.: + .tar.gz, .zip, .Z). Use the literal '>FILE<' as + a stand-in for the actual file(s) to be + extracted. For example, to count lines of code + in the input files + gcc-4.2.tar.gz perl-5.8.8.tar.gz + on Unix use + --extract-with='gzip -dc >FILE< | tar xf -' + or, if you have GNU tar, + --extract-with='tar zxf >FILE<' + and on Windows use, for example: + --extract-with="\"c:\Program Files\WinZip\WinZip32.exe\" -e -o >FILE< ." + (if WinZip is installed there). + --list-file= Take the list of file and/or directory names to + process from , which has one file/directory + name per line. Only exact matches are counted; + relative path names will be resolved starting from + the directory where cloc is invoked. + See also --exclude-list-file. + --vcs= Invoke a system call to to obtain a list of + files to work on. If is 'git', then will + invoke 'git ls-files' to get a file list and + 'git submodule status' to get a list of submodules + whose contents will be ignored. See also --git + which accepts git commit hashes and branch names. + If is 'svn' then will invoke 'svn list -R'. + The primary benefit is that cloc will then skip + files explicitly excluded by the versioning tool + in question, ie, those in .gitignore or have the + svn:ignore property. + Alternatively may be any system command + that generates a list of files. + Note: cloc must be in a directory which can read + the files as they are returned by . cloc will + not download files from remote repositories. + 'svn list -R' may refer to a remote repository + to obtain file names (and therefore may require + authentication to the remote repository), but + the files themselves must be local. + --unicode Check binary files to see if they contain Unicode + expanded ASCII text. This causes performance to + drop noticeably. + + Processing Options + --autoconf Count .in files (as processed by GNU autoconf) of + recognized languages. See also --no-autogen. + --by-file Report results for every source file encountered. + --by-file-by-lang Report results for every source file encountered + in addition to reporting by language. + --config Read command line switches from instead of + the default location of C:\Users\cbrand\AppData\Roaming\cloc. + The file should contain one switch, along with + arguments (if any), per line. Blank lines and lines + beginning with '#' are skipped. Options given on + the command line take priority over entries read from + the file. + --count-and-diff + First perform direct code counts of source file(s) + of and separately, then perform a diff + of these. Inputs may be pairs of files, directories, + or archives. If --out or --report-file is given, + three output files will be created, one for each + of the two counts and one for the diff. See also + --diff, --diff-alignment, --diff-timeout, + --ignore-case, --ignore-whitespace. + --diff Compute differences in code and comments between + source file(s) of and . The inputs + may be any mix of files, directories, archives, + or git commit hashes. Use --diff-alignment to + generate a list showing which file pairs where + compared. See also --count-and-diff, --diff-alignment, + --diff-timeout, --ignore-case, --ignore-whitespace. + --diff-timeout Ignore files which take more than seconds + to process. Default is 10 seconds. Setting + to 0 allows unlimited time. (Large files with many + repeated lines can cause Algorithm::Diff::sdiff() + to take hours.) + --docstring-as-code cloc considers docstrings to be comments, but this is + not always correct as docstrings represent regular + strings when they appear on the right hand side of an + assignment or as function arguments. This switch + forces docstrings to be counted as code. + --follow-links [Unix only] Follow symbolic links to directories + (sym links to files are always followed). + --force-lang=[,] + Process all files that have a extension + with the counter for language . For + example, to count all .f files with the + Fortran 90 counter (which expects files to + end with .f90) instead of the default Fortran 77 + counter, use + --force-lang="Fortran 90",f + If is omitted, every file will be counted + with the counter. This option can be + specified multiple times (but that is only + useful when is given each time). + See also --script-lang, --lang-no-ext. + --force-lang-def= Load language processing filters from , + then use these filters instead of the built-in + filters. Note: languages which map to the same + file extension (for example: + MATLAB/Mathematica/Objective C/MUMPS/Mercury; + Pascal/PHP; Lisp/OpenCL; Lisp/Julia; Perl/Prolog) + will be ignored as these require additional + processing that is not expressed in language + definition files. Use --read-lang-def to define + new language filters without replacing built-in + filters (see also --write-lang-def, + --write-lang-def-incl-dup). + --git Forces the inputs to be interpreted as git targets + (commit hashes, branch names, et cetera) if these + are not first identified as file or directory + names. This option overrides the --vcs=git logic + if this is given; in other words, --git gets its + list of files to work on directly from git using + the hash or branch name rather than from + 'git ls-files'. This option can be used with + --diff to perform line count diffs between git + commits, or between a git commit and a file, + directory, or archive. Use -v/--verbose to see + the git system commands cloc issues. + --ignore-whitespace Ignore horizontal white space when comparing files + with --diff. See also --ignore-case. + --ignore-case Ignore changes in case; consider upper- and lower- + case letters equivalent when comparing files with + --diff. See also --ignore-whitespace. + --lang-no-ext= Count files without extensions using the + counter. This option overrides internal logic + for files without extensions (where such files + are checked against known scripting languages + by examining the first line for #!). See also + --force-lang, --script-lang. + --max-file-size= Skip files larger than megabytes when + traversing directories. By default, =100. + cloc's memory requirement is roughly twenty times + larger than the largest file so running with + files larger than 100 MB on a computer with less + than 2 GB of memory will cause problems. + Note: this check does not apply to files + explicitly passed as command line arguments. + --no-autogen[=list] Ignore files generated by code-production systems + such as GNU autoconf. To see a list of these files + (then exit), run with --no-autogen list + See also --autoconf. + --original-dir [Only effective in combination with + --strip-comments] Write the stripped files + to the same directory as the original files. + --read-binary-files Process binary files in addition to text files. + This is usually a bad idea and should only be + attempted with text files that have embedded + binary data. + --read-lang-def= Load new language processing filters from + and merge them with those already known to cloc. + If defines a language cloc already knows + about, cloc's definition will take precedence. + Use --force-lang-def to over-ride cloc's + definitions (see also --write-lang-def, + --write-lang-def-incl-dup). + --script-lang=, Process all files that invoke as a #! + scripting language with the counter for language + . For example, files that begin with + #!/usr/local/bin/perl5.8.8 + will be counted with the Perl counter by using + --script-lang=Perl,perl5.8.8 + The language name is case insensitive but the + name of the script language executable, , + must have the right case. This option can be + specified multiple times. See also --force-lang, + --lang-no-ext. + --sdir= Use as the scratch directory instead of + letting File::Temp chose the location. Files + written to this location are not removed at + the end of the run (as they are with File::Temp). + --skip-uniqueness Skip the file uniqueness check. This will give + a performance boost at the expense of counting + files with identical contents multiple times + (if such duplicates exist). + --stdin-name= Give a file name to use to determine the language + for standard input. (Use - as the input name to + receive source code via STDIN.) + --strip-comments= For each file processed, write to the current + directory a version of the file which has blank + and commented lines removed (in-line comments + persist). The name of each stripped file is the + original file name with . appended to it. + It is written to the current directory unless + --original-dir is on. + --strip-str-comments Replace comment markers embedded in strings with + 'xx'. This attempts to work around a limitation + in Regexp::Common::Comment where comment markers + embedded in strings are seen as actual comment + markers and not strings, often resulting in a + 'Complex regular subexpression recursion limit' + warning and incorrect counts. There are two + disadvantages to using this switch: 1/code count + performance drops, and 2/code generated with + --strip-comments will contain different strings + where ever embedded comments are found. + --sum-reports Input arguments are report files previously + created with the --report-file option. Makes + a cumulative set of results containing the + sum of data from the individual report files. + --processes=NUM [Available only on systems with a recent version + of the Parallel::ForkManager module. Not + available on Windows.] Sets the maximum number of + cores that cloc uses. The default value of 0 + disables multiprocessing. + --unix Override the operating system autodetection + logic and run in UNIX mode. See also + --windows, --show-os. + --use-sloccount If SLOCCount is installed, use its compiled + executables c_count, java_count, pascal_count, + php_count, and xml_count instead of cloc's + counters. SLOCCount's compiled counters are + substantially faster than cloc's and may give + a performance improvement when counting projects + with large files. However, these cloc-specific + features will not be available: --diff, + --count-and-diff, --strip-comments, --unicode. + --windows Override the operating system autodetection + logic and run in Microsoft Windows mode. + See also --unix, --show-os. + + Filter Options + --exclude-dir=[,D2,] Exclude the given comma separated directories + D1, D2, D3, et cetera, from being scanned. For + example --exclude-dir=.cache,test will skip + all files and subdirectories that have /.cache/ + or /test/ as their parent directory. + Directories named .bzr, .cvs, .hg, .git, .svn, + and .snapshot are always excluded. + This option only works with individual directory + names so including file path separators is not + allowed. Use --fullpath and --not-match-d= + to supply a regex matching multiple subdirectories. + --exclude-ext=[,[...]] + Do not count files having the given file name + extensions. + --exclude-lang=[,L2[...]] + Exclude the given comma separated languages + L1, L2, L3, et cetera, from being counted. + --exclude-list-file= Ignore files and/or directories whose names + appear in . should have one file + name per line. Only exact matches are ignored; + relative path names will be resolved starting from + the directory where cloc is invoked. + See also --list-file. + --fullpath Modifies the behavior of --match-f, --not-match-f, + and --not-match-d to include the file's path + in the regex, not just the file's basename. + (This does not expand each file to include its + absolute path, instead it uses as much of + the path as is passed in to cloc.) + Note: --match-d always looks at the full + path and therefore is unaffected by --fullpath. + --include-ext=[,ext2[...]] + Count only languages having the given comma + separated file extensions. Use --show-ext to + see the recognized extensions. + --include-lang=[,L2[...]] + Count only the given comma separated languages + L1, L2, L3, et cetera. Use --show-lang to see + the list of recognized languages. + --match-d= Only count files in directories matching the Perl + regex. For example + --match-d='/(src|include)/' + only counts files in directories containing + /src/ or /include/. Unlike --not-match-d, + --match-f, and --not-match-f, --match-d always + compares the fully qualified path against the + regex. + --not-match-d= Count all files except those in directories + matching the Perl regex. Only the trailing + directory name is compared, for example, when + counting in /usr/local/lib, only 'lib' is + compared to the regex. + Add --fullpath to compare parent directories to + the regex. + Do not include file path separators at the + beginning or end of the regex. + --match-f= Only count files whose basenames match the Perl + regex. For example + --match-f='^[Ww]idget' + only counts files that start with Widget or widget. + Add --fullpath to include parent directories + in the regex instead of just the basename. + --not-match-f= Count all files except those whose basenames + match the Perl regex. Add --fullpath to include + parent directories in the regex instead of just + the basename. + --skip-archive= Ignore files that end with the given Perl regular + expression. For example, if given + --skip-archive='(zip|tar(.(gz|Z|bz2|xz|7z))?)' + the code will skip files that end with .zip, + .tar, .tar.gz, .tar.Z, .tar.bz2, .tar.xz, and + .tar.7z. + --skip-win-hidden On Windows, ignore hidden files. + + Debug Options + --categorized= Save names of categorized files to . + --counted= Save names of processed source files to . + --diff-alignment= Write to a list of files and file pairs + showing which files were added, removed, and/or + compared during a run with --diff. This switch + forces the --diff mode on. + --explain= Print the filters used to remove comments for + language and exit. In some cases the + filters refer to Perl subroutines rather than + regular expressions. An examination of the + source code may be needed for further explanation. + --help Print this usage information and exit. + --found= Save names of every file found to . + --ignored= Save names of ignored files and the reason they + were ignored to . + --print-filter-stages Print processed source code before and after + each filter is applied. + --show-ext[=] Print information about all known (or just the + given) file extensions and exit. + --show-lang[=] Print information about all known (or just the + given) languages and exit. + --show-os Print the value of the operating system mode + and exit. See also --unix, --windows. + -v[=] Verbose switch (optional numeric value). + -verbose[=] Long form of -v. + --version Print the version of this program and exit. + --write-lang-def= Writes to the language processing filters + then exits. Useful as a first step to creating + custom language definitions. Note: languages which + map to the same file extension will be excluded. + (See also --force-lang-def, --read-lang-def). + --write-lang-def-incl-dup= + Same as --write-lang-def, but includes duplicated + extensions. This generates a problematic language + definition file because cloc will refuse to use + it until duplicates are removed. + + Output Options + --3 Print third-generation language output. + (This option can cause report summation to fail + if some reports were produced with this option + while others were produced without it.) + --by-percent X Instead of comment and blank line counts, show + these values as percentages based on the value + of X in the denominator: + X = 'c' -> # lines of code + X = 'cm' -> # lines of code + comments + X = 'cb' -> # lines of code + blanks + X = 'cmb' -> # lines of code + comments + blanks + For example, if using method 'c' and your code + has twice as many lines of comments as lines + of code, the value in the comment column will + be 200%. The code column remains a line count. + --csv Write the results as comma separated values. + --csv-delimiter= Use the character as the delimiter for comma + separated files instead of ,. This switch forces + --file-encoding= Write output files using the encoding instead of + the default ASCII ( = 'UTF-7'). Examples: 'UTF-16', + 'euc-kr', 'iso-8859-16'. Known encodings can be + printed with + perl -MEncode -e 'print join("\n", Encode->encodings(":all")), "\n"' + --hide-rate Do not show line and file processing rates in the + output header. This makes output deterministic. + --json Write the results as JavaScript Object Notation + (JSON) formatted output. + --md Write the results as Markdown-formatted text. + --out= Synonym for --report-file=. + --progress-rate= Show progress update after every files are + processed (default =100). Set to 0 to + suppress progress output (useful when redirecting + output to STDOUT). + --quiet Suppress all information messages except for + the final report. + --report-file= Write the results to instead of STDOUT. + --sql= Write results as SQL create and insert statements + which can be read by a database program such as + SQLite. If is -, output is sent to STDOUT. + --sql-append Append SQL insert statements to the file specified + by --sql and do not generate table creation + statements. Only valid with the --sql option. + --sql-project= Use as the project identifier for the + current run. Only valid with the --sql option. + --sql-style=