├── .gitignore ├── .vscode └── launch.json ├── Assets └── DscPipelineTools │ ├── DscPipelineTools.psd1 │ ├── DscPipelineTools.psm1 │ └── Visualization.psm1 ├── DSC CI Pipeline Lab.docx ├── InfraDNS ├── Build.ps1 ├── Configs │ └── DNSServer.ps1 ├── Deploy.ps1 ├── DevEnv.ps1 └── Tests │ ├── Acceptance │ └── DNSServer.tests.ps1 │ ├── Integration │ └── DNSServer.tests.ps1 │ ├── Results │ └── Readme.md │ └── Unit │ └── DNSServer.tests.ps1 ├── Initiate.ps1 ├── LICENSE ├── Readme.md └── WebApp ├── Configs └── WebsiteConfig.ps1 ├── Deploy.ps1 ├── DevEnv.ps1 ├── TestEnv.ps1 ├── Tests ├── Acceptance │ └── Website.test.ps1 ├── Integration │ └── Website.test.ps1 └── Unit │ └── Website.test.ps1 └── build.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "PowerShell", 6 | "type": "PowerShell", 7 | "request": "launch", 8 | "program": "${file}", 9 | "args": [], 10 | "cwd": "${file}" 11 | }, 12 | { 13 | "name": "PowerShell x86", 14 | "type": "PowerShell x86", 15 | "request": "launch", 16 | "program": "${file}", 17 | "args": [], 18 | "cwd": "${file}" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /Assets/DscPipelineTools/DscPipelineTools.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'DscPipelineTools' 3 | # Generated by: MarkG 4 | # Generated on: 4/12/2016 5 | # 6 | 7 | @{ 8 | 9 | # Script module or binary module file associated with this manifest 10 | RootModule = 'DscPipelineTools.psm1' 11 | 12 | # Version number of this module. 13 | ModuleVersion = '0.1.0' 14 | 15 | # ID used to uniquely identify this module 16 | GUID = '7bffc552-224d-436d-ae38-157a796b2a82' 17 | 18 | # Author of this module 19 | Author = 'Microsoft' 20 | 21 | # Company or vendor of this module 22 | CompanyName = 'Microsoft' 23 | 24 | # Copyright statement for this module 25 | Copyright = '(c) 2016 Microsoft. All rights reserved.' 26 | 27 | # Description of the functionality provided by this module 28 | Description = 'A set of functions that helps streamline steps in a CI/CD pipeline when using PowerShell and Desired State Configuration.' 29 | 30 | # Minimum version of the Windows PowerShell engine required by this module 31 | PowerShellVersion = '4.0' 32 | 33 | # Name of the Windows PowerShell host required by this module 34 | # PowerShellHostName = '' 35 | 36 | # Minimum version of the Windows PowerShell host required by this module 37 | # PowerShellHostVersion = '' 38 | 39 | # Minimum version of the .NET Framework required by this module 40 | # DotNetFrameworkVersion = '' 41 | 42 | # Minimum version of the common language runtime (CLR) required by this module 43 | # CLRVersion = '' 44 | 45 | # Processor architecture (None, X86, Amd64) required by this module 46 | # ProcessorArchitecture = '' 47 | 48 | # Modules that must be imported into the global environment prior to importing this module 49 | # RequiredModules = @() 50 | 51 | # Assemblies that must be loaded prior to importing this module 52 | # RequiredAssemblies = @() 53 | 54 | # Script files (.ps1) that are run in the caller's environment prior to importing this module 55 | # ScriptsToProcess = @() 56 | 57 | # Type files (.ps1xml) to be loaded when importing this module 58 | # TypesToProcess = @() 59 | 60 | # Format files (.ps1xml) to be loaded when importing this module 61 | # FormatsToProcess = @() 62 | 63 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 64 | # NestedModules = @() 65 | 66 | # Functions to export from this module 67 | FunctionsToExport = '*' 68 | 69 | # Cmdlets to export from this module 70 | CmdletsToExport = '*' 71 | 72 | # Variables to export from this module 73 | VariablesToExport = '*' 74 | 75 | # Aliases to export from this module 76 | AliasesToExport = '*' 77 | 78 | # List of all modules packaged with this module 79 | # ModuleList = @() 80 | 81 | # List of all files packaged with this module 82 | # FileList = @() 83 | 84 | # Private data to pass to the module specified in RootModule/ModuleToProcess 85 | # PrivateData = '' 86 | 87 | # HelpInfo URI of this module 88 | # HelpInfoURI = '' 89 | 90 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 91 | # DefaultCommandPrefix = '' 92 | 93 | } -------------------------------------------------------------------------------- /Assets/DscPipelineTools/DscPipelineTools.psm1: -------------------------------------------------------------------------------- 1 | # Generate PowerShell Document file(s) 2 | function New-DscConfigurationDataDocument 3 | { 4 | param( 5 | [parameter(Mandatory)] 6 | [hashtable] 7 | $RawEnvData, 8 | 9 | [array] 10 | $OtherEnvData, 11 | 12 | [string] 13 | $OutputPath = '.\', 14 | 15 | [string] 16 | $FileName 17 | 18 | ) 19 | 20 | [System.Array]$AllNodesData 21 | [System.Array]$NetworkData 22 | 23 | #First validate that the path passed in is not a file 24 | if(!(Test-Path $outPutPath -IsValid) -or (Test-Path $outPutPath -PathType Leaf)) 25 | { 26 | Throw "The OutPutPath parameter must be a valid path and must not be an existing file." 27 | } 28 | 29 | if ($FileName.length -eq 0) 30 | { 31 | $FileName = $RawEnvData.Name 32 | } 33 | $OutFile = join-path $outputpath "$FileName.psd1" 34 | 35 | ## Loop through $RawEnvData and generate Configuration Document 36 | # Create AllNodes array based on input 37 | foreach ($Role in $RawEnvData.Roles) 38 | { 39 | $NumberOfServers = 0 40 | if($Role.VMQuantity -gt 0) 41 | { 42 | $NumberOfServers = $Role.VMQuantity 43 | } 44 | else 45 | { 46 | $NumberOfServers = $Role.VMName.Count 47 | } 48 | 49 | for($i = 1; $i -le $NumberOfServers; $i++) 50 | { 51 | $j = if($Role.VMQuantity -gt 0){$i} 52 | [hashtable]$NodeData = @{ NodeName = "$($Role.VMName)$j" 53 | Role = $Role.Role 54 | } 55 | # Remove Role and VMName from HT 56 | $role.remove("Role") 57 | $role.remove("VMName") 58 | 59 | # Add Lability properties to ConfigurationData if they are included in the raw hashtable 60 | if($Role.ContainsKey('VMProcessorCount')){ $NodeData += @{Lability_ProcessorCount = $Role.VMProcessorCount}} 61 | if($Role.ContainsKey('VMStartupMemory')){$NodeData += @{Lability_StartupMemory = $Role.VMStartupMemory}} 62 | if($Role.ContainsKey('NetworkName')){ $NodeData += @{Lability_SwitchName = $Role.NetworkName}} 63 | if($Role.ContainsKey('VMMedia')){ $NodeData += @{Lability_Media = $Role.VMMedia}} 64 | 65 | # Add all other properties 66 | $Role.keys | % {$NodeData += @{$_ = $Role.$_}} 67 | 68 | # Generate networking data for static networks 69 | Foreach ($Network in $OtherEnvData) 70 | { 71 | if($Network.NetworkName -eq $Role.NetworkName -and $network.IPv4AddressAssignment -eq 'Static') 72 | { 73 | # logic to add networking information 74 | } 75 | } 76 | 77 | [System.Array]$AllNodesData += $NodeData 78 | } 79 | } 80 | 81 | # Create NonNodeData hashtable based on input 82 | foreach ($Network in $OtherEnvData ) 83 | { 84 | [hashtable]$NetworkHash += @{ 85 | Name = $Network.NetworkName; 86 | Type = $Network.SwitchType; 87 | } 88 | 89 | if ($Network.ContainsKey('ExternalAdapterName')) 90 | { 91 | $NetworkHash += @{ 92 | NetAdapterName = $Network.ExternalAdapterName; 93 | AllowManagementOS = $true; 94 | } 95 | } 96 | 97 | $NetworkData += $NetworkHash 98 | } 99 | 100 | $NonNodeData = if($NetworkData){ @{Lability=@{Network = $NetworkData}}} 101 | $ConfigData = @{AllNodes = $AllNodesData; NonNodeData = $NonNodeData} 102 | 103 | 104 | if(!(Test-path $OutputPath)) 105 | { 106 | New-Item $OutputPath -ItemType Directory 107 | } 108 | 109 | import-module $PSScriptRoot\Visualization.psm1 110 | $ConfigData | convertto-ScriptBlock | Out-File $OutFile 111 | $FullFileName = dir $OutFile 112 | 113 | Return "Successfully created file $FullFileName" 114 | } 115 | 116 | # Get list of resources required by a configuration script 117 | function Get-DscRequiredResources () 118 | { 119 | param( 120 | [Parameter(Mandatory)] 121 | [string[]] 122 | $Path 123 | ) 124 | 125 | 126 | } -------------------------------------------------------------------------------- /Assets/DscPipelineTools/Visualization.psm1: -------------------------------------------------------------------------------- 1 | if ( test-path alias:print ) { remove-item alias:print } 2 | 3 | set-strictmode -version latest 4 | 5 | function ConvertTo-ScriptBlock 6 | { 7 | <# 8 | .SYNOPSIS 9 | Convert an object to a well-formatted script block. The script block can be evaluated to re-create the object. 10 | 11 | .DESCRIPTION 12 | The ConvertTo-ScriptBlock function converts an object to a well-formatted script block. The script block can be evaluated to re-create the object. 13 | 14 | The script block is useful for both visualization and serialization. It is formatted hierarchically to illustrate the relationship between an object and its children. Child objects are indented relative to their parent. The size of each indentation is controlled using the 'Indent' parameter. 15 | 16 | Child objects are traversed until a recursive reference is detected, or until the maximum depth is reached. The maximum depth is controlled using the 'Depth' parameter, and can be disabled with a value of 0. 17 | 18 | Objects are serialized by type. Many common types are supported. When a type is not supported, the object is replaced with '$null' and all of its properties are serialized in a comment block. 19 | 20 | In some cases, evaluating the script block may not re-create the entire object: 21 | 22 | - a recursive reference was found within the object 23 | - the maximum object depth was reached 24 | - an unknown object type was encountered 25 | - an internal failure occurred 26 | 27 | When this happens, a warning is issued to the caller. In addition, during an internal failure, an error is issued. 28 | 29 | .PARAMETER InputObject 30 | Specifies the object to convert. The object may also come from the pipeline. 31 | 32 | When the object is a collection, using the InputObject parameter is not the same as piping the object to ConvertTo-ScriptBlock. The pipeline processor invokes ConvertTo-ScriptBlock once for each object in the collection. The result is a collection of script blocks. 33 | 34 | .PARAMETER Indent 35 | Specifies the amount of space used when indenting a child object underneath its parent. 36 | 37 | .PARAMETER Depth 38 | Specifies the maximum depth when traversing child objects. A value of 0 disables this protective mechanism. 39 | 40 | .INPUTS 41 | Any object can be specified. 42 | 43 | .OUTPUTS 44 | System.Management.Automation.ScriptBlock 45 | 46 | .NOTES 47 | ALIASES 48 | 49 | The ConvertTo-ScriptBlock function is aliased to 'ctsb' and 'dump'. 50 | 51 | 52 | SCRIPT BLOCK INFORMATION 53 | 54 | A script block returns the output of all the commands in the script block, either as a single object or as an array. ConvertTo-ScriptBlock builds on this concept, and produces a block with a single command -- the inline creation of an object. When evaluated, the object is re-created and returned. 55 | 56 | Simple types, such as [int] and [string], have clear inline creation semantics. They appear in script blocks as [int]123 and "a string". 57 | 58 | Complex types, such as PSObject and .NET Collections (ArrayList, Stack, Queue, ...), require helper functions for inline creation. The helper functions appear in the block, and are marked as private. 59 | 60 | .EXAMPLE 61 | ConvertTo-ScriptBlock "hello, world" 62 | 63 | "hello, world" 64 | 65 | 66 | DESCRIPTION 67 | 68 | This example shows how a string is converted to a script block. 69 | 70 | .EXAMPLE 71 | ConvertTo-ScriptBlock ([Guid]::NewGuid()) 72 | 73 | [Guid]"ae00223c-19b0-4ed4-b7aa-c92d2b7b4681" 74 | 75 | 76 | DESCRIPTION 77 | 78 | This example shows how a new GUID is converted to a script block. 79 | 80 | .EXAMPLE 81 | ConvertTo-ScriptBlock $PSVersionTable 82 | 83 | @{ 84 | CLRVersion=[Version]"2.0.50727.4952"; 85 | BuildVersion=[Version]"6.1.7600.16385"; 86 | PSVersion=[Version]"2.0"; 87 | WSManStackVersion=[Version]"2.0"; 88 | PSCompatibleVersions=@( 89 | [Version]"1.0", 90 | [Version]"2.0" 91 | ); 92 | SerializationVersion=[Version]"1.1.0.1"; 93 | PSRemotingProtocolVersion=[Version]"2.1" 94 | } 95 | 96 | 97 | DESCRIPTION 98 | 99 | This example shows how a complex object is converted to a script block. $PSVersionTable is a hash table containing details about the version of Windows PowerShell that is running on the current system. It contains a number of System.Version objects, and an embedded array (PSCompatibleVersions). 100 | 101 | .EXAMPLE 102 | ConvertTo-ScriptBlock (New-Object System.Exception "sample exception") 103 | 104 | WARNING: A problem was encountered while converting the object to a script block. As a result, evaluating the script block may not re-create the object. More information may be available within the text of the script block. 105 | $null 106 | # [Exception] 107 | # Data= 108 | # [System.Collections.ListDictionaryInternal] 109 | # Count=[int]0 110 | # IsFixedSize=$false 111 | # IsReadOnly=$false 112 | # IsSynchronized=$false 113 | # Keys= 114 | # SyncRoot= 115 | # Values= 116 | # HelpLink=$null 117 | # InnerException=$null 118 | # Message="sample exception" 119 | # Source=$null 120 | # StackTrace=$null 121 | # TargetSite=$null 122 | 123 | 124 | DESCRIPTION 125 | 126 | This example shows how an unsupported object type (System.Excception) is converted to a script block. The Exception object is replaced with $null, and its properties are serialized in a comment block. 127 | 128 | Notice the warning message to the caller. 129 | 130 | .EXAMPLE 131 | ConvertTo-ScriptBlock $stack_of_pies 132 | 133 | function private:CreateStack($contents) { $o = new-object System.Collections.Stack; for ($i = $contents.Length - 1; $i -ge 0; $i--) { $x = $o.Push($contents[$i]) }; ,$o } 134 | 135 | (CreateStack @( 136 | "Pumpkin", 137 | "Blueberry", 138 | "Cherry", 139 | "Apple" 140 | )) 141 | 142 | 143 | DESCRIPTION 144 | 145 | This example shows how a System.Collections.Stack object is converted to a script block. The conversion requires the use of a helper function. 146 | 147 | The $stack_of_pies object in this example can be created using the following commands: 148 | 149 | $stack_of_pies = new-object System.Collections.Stack 150 | $stack_of_pies.Push("Apple") 151 | $stack_of_pies.Push("Cherry") 152 | $stack_of_pies.Push("Blueberry") 153 | $stack_of_pies.Push("Pumpkin") 154 | 155 | Notice that the stack ordering is preserved. 'Apple', the first element pushed, is at the bottom of the stack as shown in the script block. 'Pumpkin', the last element pushed, is at the top. The helper function inverts the array when the stack is re-created. 156 | 157 | .EXAMPLE 158 | ConvertTo-ScriptBlock $recursive_stack_of_pies 159 | 160 | WARNING: A problem was encountered while converting the object to a script block. As a result, evaluating the script block may not re-create the object. More information may be available within the text of the script block. 161 | function private:CreateStack($contents) { $o = new-object System.Collections.Stack; for ($i = $contents.Length - 1; $i -ge 0; $i--) { $x = $o.Push($contents[$i]) }; ,$o } 162 | 163 | (CreateStack @( 164 | "Pumpkin", 165 | "Blueberry", 166 | (CreateStack @( 167 | # -- contents removed (recursive reference detected) -- 168 | # -- parent object '(CreateStack @( ... ))' of type [System.Collections.Stack] at depth 0 -- 169 | )), 170 | "Cherry", 171 | "Apple" 172 | )) 173 | 174 | 175 | DESCRIPTION 176 | 177 | This example builds on the previous example, showing a similar stack. In this example, the middle of the stack contains a recursive reference. That is, one of the elements on the stack refers back to the stack itself. 178 | 179 | When a recursive element is encountered, the recursion chain is broken and a descriptive message is put in its place. The message identifies the target of the recursive reference. The target is always a parent object. 180 | 181 | The depth number is a 0-based index, starting from the top-level object, and moving down toward to the point of recursion. In this example, the message refers to depth 0 which is the top-level object. 182 | 183 | The $recursive_stack_of_pies object in this example can be created using the following commands: 184 | 185 | $recursive_stack_of_pies = new-object System.Collections.Stack 186 | $recursive_stack_of_pies.Push("Apple") 187 | $recursive_stack_of_pies.Push("Cherry") 188 | $recursive_stack_of_pies.Push($recursive_stack_of_pies) 189 | $recursive_stack_of_pies.Push("Blueberry") 190 | $recursive_stack_of_pies.Push("Pumpkin") 191 | 192 | .EXAMPLE 193 | ConvertTo-ScriptBlock $PSVersionTable -Indent 8 194 | 195 | @{ 196 | CLRVersion=[Version]"2.0.50727.4952"; 197 | BuildVersion=[Version]"6.1.7600.16385"; 198 | PSVersion=[Version]"2.0"; 199 | WSManStackVersion=[Version]"2.0"; 200 | PSCompatibleVersions=@( 201 | [Version]"1.0", 202 | [Version]"2.0" 203 | ); 204 | SerializationVersion=[Version]"1.1.0.1"; 205 | PSRemotingProtocolVersion=[Version]"2.1" 206 | } 207 | 208 | 209 | DESCRIPTION 210 | 211 | This example shows how to control the amount of space used when indenting a child object underneath its parent. 212 | 213 | .EXAMPLE 214 | ConvertTo-ScriptBlock $PSVersionTable -Depth 1 215 | 216 | WARNING: A problem was encountered while converting the object to a script block. As a result, evaluating the script block may not re-create the object. More information may be available within the text of the script block. 217 | @{ 218 | CLRVersion=[Version]"2.0.50727.4952"; 219 | BuildVersion=[Version]"6.1.7600.16385"; 220 | PSVersion=[Version]"2.0"; 221 | WSManStackVersion=[Version]"2.0"; 222 | PSCompatibleVersions=@( 223 | # -- contents removed (maximum object depth reached) -- 224 | ); 225 | SerializationVersion=[Version]"1.1.0.1"; 226 | PSRemotingProtocolVersion=[Version]"2.1" 227 | } 228 | 229 | 230 | DESCRIPTION 231 | 232 | This example shows how to control the maximum object depth when converting complex objects. The depth number is 0-based, starting from the top-level object. As child objects are visited, the depth increases. 233 | 234 | In this example, the hash table (@{ ... }) is at depth 0. The named values in the hash table (CLRVersion, BuildVersion, ...) are at depth 1. The contents of the PSCompatibleVersions array are at depth 2. 235 | 236 | With the maximum object depth set to 1, objects of depth 2 and above are replaced with a descriptive message. 237 | 238 | Notice the warning message to the caller. 239 | 240 | .LINK 241 | http://codebox/psvis 242 | 243 | .LINK 244 | about_Script_Blocks 245 | #> 246 | 247 | [CmdletBinding()] 248 | Param( 249 | [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] 250 | [AllowNull()] 251 | [AllowEmptyString()] 252 | [AllowEmptyCollection()] 253 | [object]$InputObject, 254 | 255 | [UInt32]$Indent = 4, 256 | 257 | [UInt32]$Depth = 20) 258 | 259 | Process 260 | { 261 | $dump_state = @{ 262 | scriptable = $true; # flag indicating whether or not the output script will re-create the input object 263 | indent = $Indent; # number of spaces to indent a single child object 264 | margin = 0; # current margin 265 | comment_count = 0; # counter to track how many layers are dumping within a comment block 266 | context_stack = @(); # stack of context objects being actively dumped 267 | maximum_depth = $Depth; # limit on how deep the dumper will go when traversing child objects 268 | helpers = @{}; # collection of helper functions to append to the dump output 269 | } 270 | 271 | $object_data = (DispatchObject $dump_state $InputObject $null $null) -join "`n" 272 | $helper_data = (SerializeHelpers $dump_state) -join "`n" 273 | 274 | if ($helper_data -ne $null -and $helper_data.Length -gt 0) 275 | { 276 | $str = $helper_data + "`n`n" + $object_data 277 | } 278 | else 279 | { 280 | $str = $object_data 281 | } 282 | 283 | if (-not $dump_state["scriptable"]) 284 | { 285 | write-warning "A problem was encountered while converting the object to a script block. As a result, evaluating the script block may not re-create the object. More information may be available within the text of the script block." 286 | } 287 | 288 | [ScriptBlock]::Create($str) 289 | } 290 | } 291 | 292 | 293 | ## 294 | ## module state 295 | ## 296 | 297 | $bug_report_link = "http://codebox/psvis/WorkItem/List.aspx" 298 | $handler_array = $null 299 | $handler_generic = $null 300 | $handlers_uncommented = @{} 301 | $handlers_commented = @{} 302 | $generic_customizations = @{} 303 | $ps_type_accelerators = @{} 304 | $system_namespace = "System." 305 | 306 | function Initialize 307 | { 308 | new-alias -name "ctsb" -value "ConvertTo-ScriptBlock" -scope script 309 | new-alias -name "dump" -value "ConvertTo-ScriptBlock" -scope script 310 | 311 | $script:handler_array = CreateHandler "System.Array" $null "DumpArray" "@(" ")" -is_complex 312 | $script:handler_generic = CreateHandler "" $null "DumpGeneric" $null $null -is_complex -is_generic 313 | 314 | $script:handlers_uncommented = @{} 315 | $script:handlers_commented = @{} 316 | RegisterHandler (CreateHandler "System.Collections.Hashtable" $null "DumpHashtable" "@{" "}" -is_complex) 317 | RegisterHandler (CreateHandler "system.String" $null "DumpString") 318 | RegisterHandler (CreateHandler "system.Char" $null "DumpString") 319 | RegisterHandler (CreateHandler "System.Management.Automation.ScriptBlock" $null "DumpScriptBlock") 320 | RegisterHandler (CreateHandler "System.SByte" $null "DumpRaw") 321 | RegisterHandler (CreateHandler "System.Byte" $null "DumpRaw") 322 | RegisterHandler (CreateHandler "System.Int16" $null "DumpRaw") 323 | RegisterHandler (CreateHandler "System.UInt16" $null "DumpRaw") 324 | RegisterHandler (CreateHandler "System.Int32" $null "DumpRaw") 325 | RegisterHandler (CreateHandler "System.UInt32" $null "DumpRaw") 326 | RegisterHandler (CreateHandler "System.Int64" $null "DumpRaw") 327 | RegisterHandler (CreateHandler "System.UInt64" $null "DumpRaw") 328 | RegisterHandler (CreateHandler "System.IntPtr" $null "DumpRaw") 329 | RegisterHandler (CreateHandler "System.UIntPtr" $null "DumpRaw") 330 | RegisterHandler (CreateHandler "System.Single" $null "DumpRaw") 331 | RegisterHandler (CreateHandler "System.Double" $null "DumpRaw") 332 | RegisterHandler (CreateHandler "System.Decimal" $null "DumpRaw") 333 | RegisterHandler (CreateHandler "System.DateTimeKind" $null "DumpRawQuoted") 334 | RegisterHandler (CreateHandler "System.Guid" $null "DumpRawQuoted") 335 | RegisterHandler (CreateHandler "System.TimeSpan" $null "DumpRawQuoted") 336 | RegisterHandler (CreateHandler "System.Version" $null "DumpRawQuoted") 337 | RegisterHandler (CreateHandler "System.Uri" $null "DumpRawQuoted") 338 | RegisterHandler (CreateHandler "System.Net.IPAddress" $null "DumpRawQuoted") 339 | RegisterHandler (CreateHandler "System.DateTime" $null "DumpDateTime") 340 | RegisterHandler (CreateHandler "System.Boolean" $null "DumpBoolean") 341 | RegisterHandler (CreateHandler "System.Xml.XmlDocument" $null "DumpXml") 342 | RegisterHandler (CreateHandler "System.Type" $null "DumpType") 343 | RegisterHandler (CreateHandler "System.RuntimeType" $null "DumpType") 344 | RegisterHandler -uncommented_only (CreateHandler "System.Collections.ArrayList" $null "DumpArrayList" "(CreateArrayList @(" "))" -is_complex) 345 | RegisterHandler -uncommented_only (CreateHandler "System.Collections.BitArray" $null "DumpBitArray" "(CreateBitArray @(" "))" -is_complex) 346 | RegisterHandler -uncommented_only (CreateHandler "System.Collections.Queue" $null "DumpQueue" "(CreateQueue @(" "))" -is_complex) 347 | RegisterHandler -uncommented_only (CreateHandler "System.Collections.SortedList" $null "DumpSortedList" "(CreateSortedList @{" "})" -is_complex) 348 | RegisterHandler -uncommented_only (CreateHandler "System.Collections.Stack" $null "DumpStack" "(CreateStack @(" "))" -is_complex) 349 | RegisterHandler -uncommented_only (CreateHandler "System.Text.RegularExpressions.Regex" $null "DumpRegex" "(CreateRegex @(" "))" -is_complex) 350 | RegisterHandler -uncommented_only (CreateHandler "System.Management.Automation.PSCustomObject" "PredPSObject" "DumpPSObject" "(CreatePSObject @(" "))" -is_complex) 351 | 352 | $script:generic_customizations = @{} 353 | RegisterGenericCustomization "System.Collections.ArrayList" "CustomizeCollection" 354 | RegisterGenericCustomization "System.Collections.Queue" "CustomizeCollection" 355 | RegisterGenericCustomization "System.Collections.Stack" "CustomizeCollection" 356 | RegisterGenericCustomization "System.Collections.BitArray" "CustomizeBitArray" 357 | RegisterGenericCustomization "System.Collections.SortedList" "CustomizeSortedList" 358 | 359 | $script:ps_type_accelerators = @{} 360 | $type_accelerators = @{} #([type]::gettype("System.Management.Automation.TypeAccelerators"))::Get 361 | foreach ($shortcut_type in $type_accelerators.Keys) 362 | { 363 | $full_type_name = $type_accelerators[$shortcut_type].ToString() 364 | $script:ps_type_accelerators[$full_type_name] = $shortcut_type 365 | } 366 | } 367 | 368 | 369 | 370 | ## 371 | ## framework 372 | ## 373 | 374 | function ReportInternalError([Hashtable]$dump_state, [string]$message) 375 | { 376 | $str = "Internal error." 377 | if (-not [string]::IsNullOrEmpty($message)) 378 | { 379 | $str += " " + $message 380 | } 381 | $str += " The script block may contain errors. Please submit a bug report to '" + $script:bug_report_link + "'." 382 | 383 | write-error $str 384 | 385 | if ($dump_state -ne $null) 386 | { 387 | $dump_state["scriptable"] = $false 388 | } 389 | } 390 | 391 | 392 | function CreateHandler([string]$type_name, [string]$predicate, [string]$function, [string]$prefix, [string]$suffix, [switch]$is_complex, [switch]$is_generic) 393 | { 394 | $handler = new-object psobject 395 | add-member NoteProperty -Name "type_name" -Value $type_name -InputObject $handler 396 | add-member NoteProperty -Name "predicate" -Value $predicate -InputObject $handler 397 | add-member NoteProperty -Name "function" -Value $function -InputObject $handler 398 | add-member NoteProperty -Name "prefix" -Value $prefix -InputObject $handler 399 | add-member NoteProperty -Name "suffix" -Value $suffix -InputObject $handler 400 | add-member NoteProperty -Name "is_complex" -Value $is_complex -InputObject $handler 401 | add-member NoteProperty -Name "is_generic" -Value $is_generic -InputObject $handler 402 | 403 | $handler 404 | } 405 | 406 | 407 | function RegisterHandler($handler, [switch]$commented_only, [switch]$uncommented_only) 408 | { 409 | if ($commented_only -and $uncommented_only) 410 | { 411 | ReportInternalError $null "An attempt was made to register a type handler using conflicting switches: commented_only and uncommented_only." 412 | } 413 | elseif ($commented_only) 414 | { 415 | $script:handlers_commented[$handler.type_name.ToLower()] = $handler 416 | } 417 | elseif ($uncommented_only) 418 | { 419 | $script:handlers_uncommented[$handler.type_name.ToLower()] = $handler 420 | } 421 | else 422 | { 423 | $script:handlers_commented[$handler.type_name.ToLower()] = $handler 424 | $script:handlers_uncommented[$handler.type_name.ToLower()] = $handler 425 | } 426 | } 427 | 428 | 429 | function GetHandler([Hashtable]$dump_state, $context) 430 | { 431 | if ($dump_state["comment_count"] -gt 0) 432 | { 433 | $script:handlers_commented[$context.type_name.ToLower()] 434 | } 435 | else 436 | { 437 | $script:handlers_uncommented[$context.type_name.ToLower()] 438 | } 439 | } 440 | 441 | 442 | function RegisterGenericCustomization([string]$type_name, [string]$customization) 443 | { 444 | $script:generic_customizations[$type_name.ToLower()] = $customization 445 | } 446 | 447 | 448 | function GetGenericCustomization([string]$type_name) 449 | { 450 | $script:generic_customizations[$type_name.ToLower()] 451 | } 452 | 453 | 454 | function CreateObjectContext([Hashtable]$dump_state, [object]$o, [string]$prefix, [string]$suffix) 455 | { 456 | $context = new-object psobject 457 | add-member NoteProperty -Name "o" -Value $o -InputObject $context 458 | add-member NoteProperty -Name "ref" -Value ([ref]$o) -InputObject $context 459 | add-member NoteProperty -Name "prefix" -Value $prefix -InputObject $context 460 | add-member NoteProperty -Name "suffix" -Value $suffix -InputObject $context 461 | 462 | # collect type information for the object. 463 | if ($o -eq $null) 464 | { 465 | # $null objects don't have type information. 466 | add-member NoteProperty -Name "type_name" -Value "" -InputObject $context 467 | add-member NoteProperty -Name "base_type_name" -Value "" -InputObject $context 468 | add-member NoteProperty -Name "resolved_type_name" -Value "" -InputObject $context 469 | } 470 | else 471 | { 472 | # the GetType() method, found on nearly all objects, yields the most fidelity. 473 | # some objects, such as those in powershell's Mobile Object Model, don't support 474 | # this method, hence the try/catch block. 475 | try 476 | { 477 | $type = $o.GetType() 478 | add-member NoteProperty -Name "type_name" -Value $type.FullName -InputObject $context 479 | if ($type.BaseType -ne $null) 480 | { 481 | add-member NoteProperty -Name "base_type_name" -Value $type.BaseType.FullName -InputObject $context 482 | } 483 | else 484 | { 485 | # there is no base type. use the object type as the base type. 486 | add-member NoteProperty -Name "base_type_name" -Value $type.FullName -InputObject $context 487 | } 488 | } 489 | catch 490 | { 491 | # fall back on get-member to retrieve type information. it returns a record for 492 | # each child member, and the records all contain type information about the object. 493 | # the type information is identical across all member records. 494 | # 495 | # base type information is not available through get-member. assume that in these 496 | # cases where GetType() is missing, there is no base type. use the object type as 497 | # the base type. 498 | $members = @(get-member -inputobject $o) 499 | if ($members -ne $null -and $members.Length -gt 0 -and $members[0].TypeName -ne $null) 500 | { 501 | add-member NoteProperty -Name "type_name" -Value $members[0].TypeName -InputObject $context 502 | add-member NoteProperty -Name "base_type_name" -Value $members[0].TypeName -InputObject $context 503 | } 504 | else 505 | { 506 | ReportInternalError $dump_state "Unable to collect type information for the given object." 507 | } 508 | } 509 | 510 | # try to resolve the type name using a list of known "powershell type accelerators". 511 | # these are effectively type aliases. e.g. [System.Int32] maps to [int]. 512 | $resolved_type_name = $script:ps_type_accelerators[$context.type_name] 513 | if ([string]::IsNullOrEmpty($resolved_type_name)) 514 | { 515 | # if the type is in the system namespace, remove the "system." prefix. powershell 516 | # recognizes classes and structures from the system namespace without fully qualified 517 | # type name references. 518 | if ($context.type_name.StartsWith($script:system_namespace)) 519 | { 520 | $index_of_dot = $context.type_name.IndexOf(".") 521 | if ($index_of_dot -ge 0 -and $index_of_dot -eq $context.type_name.LastIndexOf(".")) 522 | { 523 | $resolved_type_name = $context.type_name.Substring($script:system_namespace.Length) 524 | } 525 | } 526 | } 527 | 528 | if ([string]::IsNullOrEmpty($resolved_type_name)) 529 | { 530 | # the type name cannot be resolved 531 | $resolved_type_name = $context.type_name 532 | } 533 | 534 | add-member NoteProperty -Name "resolved_type_name" -Value $resolved_type_name -InputObject $context 535 | } 536 | 537 | $context 538 | } 539 | 540 | 541 | function DispatchObject([Hashtable]$dump_state, [object]$o, [string]$prefix, [string]$suffix) 542 | { 543 | if ($o -eq $null) 544 | { 545 | # null values don't carry type information, which means they cannot be dispatched 546 | # through the handler table. 547 | Print $dump_state ($prefix + '$null' + $suffix) 548 | } 549 | else 550 | { 551 | $context = CreateObjectContext $dump_state $o $prefix $suffix 552 | 553 | # arrays are dispatched by their base type (system.array) rather than their actual 554 | # type (object[], byte[], ...). the assumption is that no one will be deriving from 555 | # System.Array to make their own type. 556 | if ($context.base_type_name -eq "System.Array") 557 | { 558 | $handler = $script:handler_array 559 | } 560 | else 561 | { 562 | $handler = GetHandler $dump_state $context 563 | if ($handler -ne $null -and -not [string]::IsNullOrEmpty($handler.predicate)) 564 | { 565 | if (-not (& $handler.predicate $dump_state $context)) 566 | { 567 | $handler = $null 568 | } 569 | } 570 | 571 | if ($handler -eq $null) 572 | { 573 | # the object type is not recognized. dump it using a generic handler. 574 | $handler = $script:handler_generic 575 | } 576 | } 577 | 578 | add-member NoteProperty -Name "handler" -Value $handler -InputObject $context 579 | 580 | if ($handler.is_complex) 581 | { 582 | DispatchComplexObject $dump_state $context 583 | } 584 | else 585 | { 586 | DispatchSimpleObject $dump_state $context 587 | } 588 | } 589 | } 590 | 591 | 592 | function DispatchComplexObject([Hashtable]$dump_state, $context) 593 | { 594 | $handler = $context.handler 595 | 596 | $prefix = $context.prefix + $handler.prefix 597 | $suffix = $handler.suffix + $context.suffix 598 | 599 | if ($handler.is_generic) 600 | { 601 | # the object type is not recognized, and will be dumped in a comment block 602 | # via the generic handler. the object will be replaced with $null. 603 | if ($dump_state["comment_count"] -gt 0) 604 | { 605 | # a comment block is in effect. don't print the '$null' placeholder, as 606 | # it is unnecessary and confusing. also, don't print the suffix. it 607 | # makes no sense without the '$null' placeholder. 608 | $prefix = $context.prefix 609 | $suffix = $null 610 | } 611 | else 612 | { 613 | $prefix = $context.prefix + '$null' + $context.suffix 614 | $suffix = $null 615 | } 616 | } 617 | 618 | if ($prefix -ne $null -and $prefix -ne "") 619 | { 620 | Print $dump_state $prefix 621 | } 622 | 623 | Indent $dump_state 624 | 625 | $recursion_index = GetIndexOfRecursiveReference $dump_state $context.ref 626 | if ($recursion_index -ge 0) 627 | { 628 | # record the fact a cycle was found in the object graph 629 | $dump_state["scriptable"] = $false 630 | 631 | $recursion_context = ($dump_state["context_stack"])[$recursion_index] 632 | $recursion_prefix = $recursion_context.prefix + $recursion_context.handler.prefix 633 | $recursion_suffix = $recursion_context.handler.suffix + $recursion_context.suffix 634 | 635 | $recursion_message = "-- parent object" 636 | if (-not ([string]::IsNullOrEmpty($recursion_prefix) -and [string]::IsNullOrEmpty($recursion_suffix))) 637 | { 638 | $recursion_message += (" '{0} ... {1}'" -f ($recursion_prefix, $recursion_suffix)) 639 | } 640 | $recursion_message += " of type [" + $recursion_context.resolved_type_name + "] at depth " + $recursion_index + " --" 641 | 642 | Comment $dump_state 643 | Print $dump_state "-- contents removed (recursive reference detected) --" 644 | Print $dump_state $recursion_message 645 | Uncomment $dump_state 646 | } 647 | else 648 | { 649 | if (PushContext $dump_state $context) 650 | { 651 | DispatchSimpleObject $dump_state $context 652 | PopContext $dump_state 653 | } 654 | else 655 | { 656 | # record the fact the maximum object depth was exceeded 657 | $dump_state["scriptable"] = $false 658 | 659 | Comment $dump_state 660 | Print $dump_state "-- contents removed (maximum object depth reached) --" 661 | Uncomment $dump_state 662 | } 663 | } 664 | 665 | Unindent $dump_state 666 | 667 | if ($suffix -ne $null -and $suffix -ne "") 668 | { 669 | Print $dump_state $suffix 670 | } 671 | } 672 | 673 | 674 | function DispatchSimpleObject([Hashtable]$dump_state, $context) 675 | { 676 | & $context.handler.function $dump_state $context 677 | } 678 | 679 | 680 | function Indent([Hashtable]$dump_state) 681 | { 682 | $dump_state["margin"] += $dump_state["indent"] 683 | } 684 | 685 | 686 | function Unindent([Hashtable]$dump_state) 687 | { 688 | $dump_state["margin"] -= $dump_state["indent"] 689 | } 690 | 691 | 692 | function Comment([Hashtable]$dump_state) 693 | { 694 | $dump_state["comment_count"]++ 695 | } 696 | 697 | 698 | function Uncomment([Hashtable]$dump_state) 699 | { 700 | $dump_state["comment_count"]-- 701 | } 702 | 703 | 704 | function Print([Hashtable]$dump_state, [string]$data) 705 | { 706 | $margin = $dump_state["margin"] 707 | if ($dump_state["comment_count"] -gt 0) 708 | { 709 | if ($margin -ge 2) 710 | { 711 | $margin -= 2 712 | } 713 | "# " + "".PadLeft($margin) + $data 714 | } 715 | else 716 | { 717 | "".PadLeft($margin) + $data 718 | } 719 | } 720 | 721 | 722 | function IsDoubleRef([ref]$r) 723 | { 724 | $double_ref = $false 725 | if ($r.Value -ne $null) 726 | { 727 | $members = @(get-member -InputObject $r.Value) 728 | if ($members -ne $null -and $members.Length -gt 0) 729 | { 730 | $double_ref = $members[0].TypeName -eq "System.Management.Automation.PSReference" 731 | } 732 | } 733 | $double_ref 734 | } 735 | 736 | 737 | function DoRefsMatch([ref]$r1, [ref]$r2) 738 | { 739 | # use [Object]::ReferenceEquals(r1, r2) to compare the underlying object 740 | # pointer in each reference. 741 | # 742 | # [Object]::ReferenceEquals() can only handle references to objects. It 743 | # cannot handle references to references. In those cases, the -eq operator 744 | # will be used to compare the underlying references. 745 | # 746 | # Consider this example: 747 | # 748 | # $x = 1234 749 | # $y = [ref]$x 750 | # $x = [ref]$y 751 | # 752 | # This creates a circular reference: x -> y -> x -> y ... 753 | # 754 | # $x.Value refers to $y. $y.Value refers to $x. Dereferencing $x 755 | # twice leads back to $x: 756 | # 757 | # $x -eq $x ====> $true 758 | # $x -eq $x.Value ====> $false 759 | # $x -eq $x.Value.Value ====> $true 760 | # 761 | # The [ref] objects refer to each other, and -eq can be used to tell them 762 | # apart. This fills in the missing behavior from [Object]::ReferenceEquals. 763 | 764 | $r1_double_ref = IsDoubleRef $r1 765 | $r2_double_ref = IsDoubleRef $r2 766 | if ($r1_double_ref -and $r2_double_ref) 767 | { 768 | # compare the underlying reference objects, which are part of the object structure 769 | # being dumped. they will not change, and will refer to each other (eventually) 770 | # in the case of a recursively defined object. 771 | # 772 | # do not compare the references themselves, as they are likely to be different 773 | # reference objects. this is true even when they refer to the same underlying 774 | # object. 775 | $r1.Value -eq $r2.Value 776 | } 777 | elseif ($r1_double_ref -or $r2_double_ref) 778 | { 779 | # one of the references is a double-ref, the other is not. they cannot be equal. 780 | $false 781 | } 782 | else 783 | { 784 | [Object]::ReferenceEquals($r1.Value, $r2.Value) 785 | } 786 | } 787 | 788 | 789 | function GetIndexOfRecursiveReference([Hashtable]$dump_state, [ref]$r) 790 | { 791 | $index = 0 792 | foreach ($context in $dump_state["context_stack"]) 793 | { 794 | if (DoRefsMatch $context.ref $r) 795 | { 796 | $index 797 | return 798 | } 799 | $index++ 800 | } 801 | 802 | -1 803 | } 804 | 805 | 806 | function PushContext([Hashtable]$dump_state, $context) 807 | { 808 | if ($dump_state["maximum_depth"] -gt 0 -and 809 | $dump_state["context_stack"].Length -ge $dump_state["maximum_depth"]) 810 | { 811 | $false 812 | } 813 | else 814 | { 815 | $dump_state["context_stack"] += $context 816 | $true 817 | } 818 | } 819 | 820 | 821 | function PopContext([Hashtable]$dump_state) 822 | { 823 | $stack = $dump_state["context_stack"] 824 | if ($stack.Length -gt 0) 825 | { 826 | $stack_updated = @() 827 | for ($index = 0; $index -lt ($stack.Length - 1); $index++) 828 | { 829 | $stack_updated += $stack[$index] 830 | } 831 | 832 | $dump_state["context_stack"] = $stack_updated 833 | } 834 | } 835 | 836 | 837 | function RegisterHelper([Hashtable]$dump_state, [string]$helper) 838 | { 839 | $func = get-item function:\$helper 840 | if ($func -ne $null) 841 | { 842 | ($dump_state["helpers"])[$helper.ToLower()] = $func 843 | } 844 | else 845 | { 846 | ReportInternalError $dump_state "An attempt was made to register a helper function named '$helper', but that function does not exist." 847 | } 848 | } 849 | 850 | 851 | function SerializeHelpers([Hashtable]$dump_state) 852 | { 853 | $helpers = $dump_state["helpers"] 854 | foreach ($helper in $helpers.Keys) 855 | { 856 | SerializeFunctionAsPrivate $helpers[$helper] 857 | } 858 | } 859 | 860 | 861 | function SerializeFunctionAsPrivate($func) 862 | { 863 | # assume a simple function definition. don't handle parameter types, 864 | # don't handle anything related to advanced functions, such as parameter 865 | # sets, attributes, and aliases. 866 | $str = "function private:" + $func.Name 867 | if ($func.Parameters -ne $null -and $func.Parameters.Count -gt 0) 868 | { 869 | $str += "(" 870 | $first_param = $true 871 | foreach ($param_name in $func.Parameters.Keys) 872 | { 873 | if ($first_param) 874 | { 875 | $first_param = $false 876 | } 877 | else 878 | { 879 | $str += ", " 880 | } 881 | $str += '$' + $param_name 882 | } 883 | $str += ")" 884 | } 885 | 886 | # strip off the 'param(' block at the front of the definition. 887 | # always on its own line at the start of the definition. 888 | $start_second_line = $func.Definition.IndexOf("`n") 889 | $body = $func.Definition.Substring($start_second_line + 1).Trim() 890 | 891 | $str + " { " + $body + " }" 892 | } 893 | 894 | 895 | 896 | ## 897 | ## type-specific handlers and customizations 898 | ## 899 | 900 | function DumpArray([Hashtable]$dump_state, $context) 901 | { 902 | DumpArrayObject $dump_state $context.o 903 | } 904 | 905 | 906 | function DumpArrayObject([Hashtable]$dump_state, $o) 907 | { 908 | $array = @($o) 909 | $element_suffix = "," 910 | for ($i = 0; $i -lt $array.Length; $i++) 911 | { 912 | if ($i -eq ($array.Length - 1)) 913 | { 914 | $element_suffix = $null 915 | } 916 | DispatchObject $dump_state $array[$i] $null $element_suffix 917 | } 918 | } 919 | 920 | 921 | function DumpGeneric([Hashtable]$dump_state, $context) 922 | { 923 | # this object's type is unrecognized. re-creating it will be impossible 924 | # because the construction semantics are unknown. 925 | # 926 | # replace the object with '$null', and dump its property members out 927 | # using a multi-line comment block: 928 | # 929 | # $null 930 | # # [Fabrikam.Phone] 931 | # # Connected=[bool]$true 932 | # # SpeedDial=@( 933 | # # "5551234", 934 | # # "5559988" 935 | # # ) 936 | 937 | # record the fact the an unscriptable object was encountered 938 | $dump_state["scriptable"] = $false 939 | 940 | # collect information on object members, and any customizations 941 | # required when printing this object. 942 | $members = @(get-member -MemberType Properties -InputObject $context.o) 943 | 944 | $members_to_skip = @{} 945 | $members_to_add = @{} 946 | 947 | $customization = GetGenericCustomization $context.type_name 948 | if ($customization -ne $null -and $customization -ne "") 949 | { 950 | & $customization $context $members_to_skip $members_to_add 951 | } 952 | 953 | # see if there's any data to display. if not, don't print the type. 954 | # leave the display completely empty. 955 | if ($members.Length -le 0 -and $members_to_skip.Count -le 0 -and $members_to_add.Count -le 0) 956 | { 957 | return 958 | } 959 | 960 | Comment $dump_state 961 | Print $dump_state ("[" + $context.resolved_type_name + "]") 962 | 963 | foreach ($member in $members) 964 | { 965 | if (-not $members_to_skip.ContainsKey($member.Name)) 966 | { 967 | DispatchObject $dump_state $context.o.($member.Name) ($member.Name + "=") $null 968 | } 969 | } 970 | 971 | foreach ($member_name in $members_to_add.Keys) 972 | { 973 | DispatchObject $dump_state $members_to_add[$member_name] ($member_name + "=") $null 974 | } 975 | 976 | Uncomment $dump_state 977 | } 978 | 979 | 980 | function DumpHashtable([Hashtable]$dump_state, $context) 981 | { 982 | $array = @($context.o.Keys) 983 | $element_suffix = ";" 984 | for ($i = 0; $i -lt $array.Length; $i++) 985 | { 986 | $key = $array[$i] 987 | if ($i -eq ($array.Length - 1)) 988 | { 989 | $element_suffix = $null 990 | } 991 | DispatchObject $dump_state $context.o[$key] ($key + "=") $element_suffix 992 | } 993 | } 994 | 995 | 996 | function DumpString([Hashtable]$dump_state, $context) 997 | { 998 | # this method serializes a string, such that it can survive a roundtrip though a scriptblock. 999 | # when the block is evaluated, the string is re-created. it should be identical to the 1000 | # original string. furthermore, the serialized version of the string (held in the block), 1001 | # should be human-readable (e.g. not filled with numeric character codes, binary data, ...). 1002 | 1003 | # the output of this method will be code that embodies a double-quoted string. that means the 1004 | # rules of character escaping and variable substitution are in effect. 1005 | $s = $context.o.ToString() 1006 | 1007 | # start by escaping the escape character 1008 | $s = $s.Replace('`', '``') 1009 | 1010 | # escape embedded single-quote characters that might prematurely terminate the string 1011 | $s = $s.Replace("'", "`'") 1012 | 1013 | # escape variable references so they are not evaluated when the scriptblock is executed 1014 | $s = $s.Replace('$', '`$') 1015 | 1016 | # scriptblocks eat tabs, for some reason. preserve each tab by replacing it with an 1017 | # escape sequence. the sequence will be evaluated when the scriptblock runs. 1018 | $s = $s.Replace("`t", '`t') 1019 | 1020 | # replace all other special characters with their escape sequences. this is not strictly 1021 | # necessary, but it does help to reveal with contents of the string. the assumption here 1022 | # is that most consumers of this library will be more interested in seeing the contents, 1023 | # and less interested in seeing the final rendered form of the string. 1024 | $s = $s.Replace("`0", '`0') 1025 | $s = $s.Replace("`a", '`a') 1026 | $s = $s.Replace("`b", '`b') 1027 | $s = $s.Replace("`f", '`f') 1028 | $s = $s.Replace("`v", '`v') 1029 | $s = $s.Replace("`r", '`r') 1030 | 1031 | # break the string up into an array of lines in preparation for output 1032 | $lines = $s -split "`n" 1033 | 1034 | # finally, print the string 1035 | $resolved_type_name = "" 1036 | if ($context.type_name -ne "system.string") 1037 | { 1038 | $resolved_type_name = "[" + $context.resolved_type_name + "]" 1039 | } 1040 | 1041 | if ($lines.Length -eq 1) 1042 | { 1043 | Print $dump_state ($context.prefix + $resolved_type_name + "'" + $lines[0] + "'" + $context.suffix) 1044 | } 1045 | else 1046 | { 1047 | Print $dump_state ($context.prefix + $resolved_type_name + "@" + "'") 1048 | 1049 | # do not indent the contents of the here-string, or the terminating marker. 1050 | # any indentation will become part of the here-string during deserialization. 1051 | $margin = $dump_state["margin"] 1052 | $dump_state["margin"] = 0 1053 | foreach ($line in $lines) 1054 | { 1055 | Print $dump_state $line 1056 | } 1057 | Print $dump_state ("'" + "@" + $context.suffix) 1058 | $dump_state["margin"] = $margin 1059 | } 1060 | } 1061 | 1062 | 1063 | function DumpScriptBlock([Hashtable]$dump_state, $context) 1064 | { 1065 | # serialize the script block. cleanup leading and trailing blank lines. 1066 | # replace tabs with spaces in preparation for finding the left margin 1067 | # for proper indentation. 1068 | $strings = $context.o.ToString().Replace("`r`n","`n") -split "`n" 1069 | $strings = @(TrimFirstAndLastStringIfEmpty $strings) 1070 | $strings = @(ExpandLeadingTabsToSpaces $strings) 1071 | 1072 | # find the width of the left margin. trim the margin space from each 1073 | # line, shifting the block as far left as possible without losing data. 1074 | $shortest_block_indent = GetLengthOfShortestIndentation $strings 1075 | if ($shortest_block_indent -gt 0) 1076 | { 1077 | $strings_trimmed = @() 1078 | foreach ($string in $strings) 1079 | { 1080 | if ($string -ne $null -and $shortest_block_indent -le $string.Length) 1081 | { 1082 | $string = $string.Substring($shortest_block_indent) 1083 | } 1084 | $strings_trimmed += $string 1085 | } 1086 | $strings = $strings_trimmed 1087 | } 1088 | 1089 | # finally, print the script block 1090 | Print $dump_state ($context.prefix + "{") 1091 | Indent $dump_state 1092 | foreach ($string in $strings) 1093 | { 1094 | Print $dump_state $string 1095 | } 1096 | Unindent $dump_state 1097 | Print $dump_state ("}" + $context.suffix) 1098 | } 1099 | 1100 | 1101 | function TrimFirstAndLastStringIfEmpty([string[]]$array) 1102 | { 1103 | $array_trimmed = @() 1104 | if ($array -ne $null -and $array.Length -gt 0) 1105 | { 1106 | $start = 0 1107 | if ($array[0] -eq $null -or $array[0].Trim(" `t`r`n") -eq "") 1108 | { 1109 | $start++ 1110 | } 1111 | 1112 | $end = $array.Length - 1 1113 | if ($array[$end] -eq $null -or $array[$end].Trim(" `t`r`n") -eq "") 1114 | { 1115 | $end-- 1116 | } 1117 | 1118 | if ($start -le $end) 1119 | { 1120 | $array_trimmed = $array[$start .. $end] 1121 | } 1122 | } 1123 | $array_trimmed 1124 | } 1125 | 1126 | 1127 | function ExpandLeadingTabsToSpaces([string[]]$array) 1128 | { 1129 | $array_expanded = @() 1130 | foreach ($string in $array) 1131 | { 1132 | $string_expanded = "" 1133 | for ($i = 0; $i -lt $string.Length; $i++) 1134 | { 1135 | if ($string[$i] -eq " ") 1136 | { 1137 | $string_expanded += " " 1138 | } 1139 | elseif ($string[$i] -eq "`t") 1140 | { 1141 | $string_expanded += "".PadLeft(8 - ($string_expanded.Length % 8)) 1142 | } 1143 | else 1144 | { 1145 | $string_expanded += $string.Substring($i) 1146 | break 1147 | } 1148 | } 1149 | 1150 | $array_expanded += $string_expanded 1151 | } 1152 | $array_expanded 1153 | } 1154 | 1155 | 1156 | function GetLengthOfShortestIndentation([string[]]$array) 1157 | { 1158 | $first_line = $true 1159 | $shortest_indent = 0 1160 | 1161 | foreach ($string in $array) 1162 | { 1163 | # ignore blank/empty lines 1164 | 1165 | if ($string -ne $null -and $string.Trim(" `t").Length -gt 0) 1166 | { 1167 | $indent = 0 1168 | if ($string -match "^ +") 1169 | { 1170 | $indent = $matches[0].Length 1171 | } 1172 | 1173 | if ($first_line -or $shortest_indent -gt $indent) 1174 | { 1175 | $shortest_indent = $indent 1176 | $first_line = $false 1177 | } 1178 | } 1179 | } 1180 | 1181 | $shortest_indent 1182 | } 1183 | 1184 | 1185 | function DumpRaw([Hashtable]$dump_state, $context) 1186 | { 1187 | #Print $dump_state ($context.prefix + "[" + $context.resolved_type_name + "]" + $context.o.ToString() + $context.suffix) 1188 | Print $dump_state ($context.prefix + $context.o.ToString() + $context.suffix) 1189 | } 1190 | 1191 | 1192 | function DumpRawQuoted([Hashtable]$dump_state, $context) 1193 | { 1194 | #Print $dump_state ($context.prefix + "[" + $context.resolved_type_name + ']"' + $context.o.ToString() + '"' + $context.suffix) 1195 | Print $dump_state ($context.prefix + "'" + $context.o.ToString() + "'" + $context.suffix) 1196 | } 1197 | 1198 | 1199 | function DumpBoolean([Hashtable]$dump_state, $context) 1200 | { 1201 | if ($context.o) 1202 | { 1203 | Print $dump_state ($context.prefix + '$true' + $context.suffix) 1204 | } 1205 | else 1206 | { 1207 | Print $dump_state ($context.prefix + '$false' + $context.suffix) 1208 | } 1209 | } 1210 | 1211 | 1212 | function DumpDateTime([Hashtable]$dump_state, $context) 1213 | { 1214 | Print $dump_state ($context.prefix + '[' + $context.resolved_type_name + "]'" + $context.o.ToString("O") + "'" + $context.suffix) 1215 | } 1216 | 1217 | 1218 | function DumpXml([Hashtable]$dump_state, $context) 1219 | { 1220 | # serialize the XmlDocument object using an XmlTextWriter 1221 | # backed by a StringBuilder. collect the results, and hand 1222 | # them off to the string dumper. 1223 | 1224 | $text_writer = $null 1225 | $writer = $null 1226 | $str = "" 1227 | try 1228 | { 1229 | $text_writer = new-object System.IO.StringWriter 1230 | 1231 | $writer = new-object system.Xml.XmlTextWriter @($text_writer) 1232 | $writer.Formatting = [System.Xml.Formatting]::Indented 1233 | $writer.Indentation = $dump_state["indent"] 1234 | 1235 | $context.o.WriteTo($writer) 1236 | $str = $text_writer.ToString().Replace("`r`n","`n") 1237 | } 1238 | finally 1239 | { 1240 | if ($text_writer -ne $null) 1241 | { 1242 | $text_writer.Close() 1243 | $text_writer.Dispose() 1244 | } 1245 | if ($writer -ne $null) 1246 | { 1247 | $writer.Close() 1248 | } 1249 | } 1250 | 1251 | # replace the XmlDocument object with its string equivalent. leave the 1252 | # type information in place, as it is needed by the string dumper. 1253 | 1254 | add-member NoteProperty -Name "o" -Value $str -InputObject $context -Force 1255 | add-member NoteProperty -Name "ref" -Value ([ref]$str) -InputObject $context -Force 1256 | 1257 | DumpString $dump_state $context 1258 | } 1259 | 1260 | 1261 | function DumpType([Hashtable]$dump_state, $context) 1262 | { 1263 | Print $dump_state ($context.prefix + "[" + $context.o.FullName + "]" + $context.suffix) 1264 | } 1265 | 1266 | 1267 | function CreateArrayList($contents) { $o = new-object System.Collections.ArrayList; $contents | % { $x = $o.Add($_) }; ,$o } 1268 | 1269 | function DumpArrayList([Hashtable]$dump_state, $context) 1270 | { 1271 | DumpArray $dump_state $context 1272 | RegisterHelper $dump_state "CreateArrayList" 1273 | } 1274 | 1275 | 1276 | function CreateBitArray($contents) { $o = new-object System.Collections.BitArray @($contents.Length); for ($i = 0; $i -lt $contents.Length; $i++) { $o[$i] = $contents[$i] }; ,$o } 1277 | 1278 | function DumpBitArray([Hashtable]$dump_state, $context) 1279 | { 1280 | DumpArray $dump_state $context 1281 | RegisterHelper $dump_state "CreateBitArray" 1282 | } 1283 | 1284 | 1285 | function CreateQueue($contents) { $o = new-object System.Collections.Queue; $contents | % { $x = $o.Enqueue($_) }; ,$o } 1286 | 1287 | function DumpQueue([Hashtable]$dump_state, $context) 1288 | { 1289 | DumpArray $dump_state $context 1290 | RegisterHelper $dump_state "CreateQueue" 1291 | } 1292 | 1293 | 1294 | function CreateSortedList($contents) { $o = new-object System.Collections.SortedList; $contents.Keys | % { $x = $o.Add($_, $contents[$_]) }; ,$o } 1295 | 1296 | function DumpSortedList([Hashtable]$dump_state, $context) 1297 | { 1298 | DumpHashtable $dump_state $context 1299 | RegisterHelper $dump_state "CreateSortedList" 1300 | } 1301 | 1302 | 1303 | function CreateStack($contents) { $o = new-object System.Collections.Stack; for ($i = $contents.Length - 1; $i -ge 0; $i--) { $x = $o.Push($contents[$i]) }; ,$o } 1304 | 1305 | function DumpStack([Hashtable]$dump_state, $context) 1306 | { 1307 | DumpArray $dump_state $context 1308 | RegisterHelper $dump_state "CreateStack" 1309 | } 1310 | 1311 | 1312 | function CreateRegex($contents) { ,(new-object System.Text.RegularExpressions.Regex @($contents[0], $contents[1])) } 1313 | 1314 | function DumpRegex([Hashtable]$dump_state, $context) 1315 | { 1316 | DispatchObject $dump_state $context.o.ToString() $null "," 1317 | DispatchObject $dump_state ([int]$context.o.Options) $null $null 1318 | RegisterHelper $dump_state "CreateRegex" 1319 | } 1320 | 1321 | 1322 | function CreatePSObject($contents) { $o = new-object psobject; $contents | % { $o | add-member @_ }; ,$o } 1323 | 1324 | function PredPSObject([Hashtable]$dump_state, $context) 1325 | { 1326 | DumpPSObject $dump_state $context -predicate 1327 | } 1328 | 1329 | 1330 | function DumpPSObject([Hashtable]$dump_state, $context, [switch]$predicate) 1331 | { 1332 | $can_serialize = $true 1333 | $contents = @() 1334 | 1335 | $members = @(get-member -inputobject $o) 1336 | foreach ($member in $members) 1337 | { 1338 | if ($member.MemberType -eq "Method") 1339 | { 1340 | # allow standard methods common to all psconfig objects. these will be 1341 | # created automatically. fail on any other unrecognized method. 1342 | if ($member.Name -ne "Equals" -and $member.Name -ne "GetHashCode" -and 1343 | $member.Name -ne "GetType" -and $member.Name -ne "ToString") 1344 | { 1345 | $can_serialize = $false 1346 | break 1347 | } 1348 | } 1349 | elseif ($member.MemberType -eq "NoteProperty") 1350 | { 1351 | $contents += @{MemberType = $member.MemberType.ToString(); Name = $member.Name; Value = $o.($member.Name)} 1352 | } 1353 | elseif ($member.MemberType -eq "AliasProperty") 1354 | { 1355 | # extract the property being aliased. look for the target type as well, if specified. 1356 | if ($member.Definition -match ('^' + $member.Name + ' = \(([A-Za-z0-9_.]{1,})\)([A-Za-z0-9_]{1,})$')) 1357 | { 1358 | $contents += @{MemberType = $member.MemberType.ToString(); Name = $member.Name; Value = $Matches[2]; SecondValue = $Matches[1]} 1359 | } 1360 | elseif ($member.Definition -match ('^' + $member.Name + ' = ([A-Za-z0-9_]{1,})$')) 1361 | { 1362 | $contents += @{MemberType = $member.MemberType.ToString(); Name = $member.Name; Value = $Matches[1]} 1363 | } 1364 | else 1365 | { 1366 | $can_serialize = $false 1367 | break 1368 | } 1369 | } 1370 | elseif ($member.MemberType -eq "ScriptMethod") 1371 | { 1372 | $contents += @{MemberType = $member.MemberType.ToString(); Name = $member.Name; Value = $o.($member.Name).Script} 1373 | } 1374 | else 1375 | { 1376 | # MemberSet / PropertySet / ParameterizedProperty 1377 | # These may be reproducable. They are worth further investigation if demand warrants it. 1378 | # ScriptProperty 1379 | # The definition string is not precise enough to separate the getter and the optional 1380 | # setter. the token which separates the two (';set=') could appear in either block, 1381 | # causing a mismatch. 1382 | $can_serialize = $false 1383 | break 1384 | } 1385 | } 1386 | 1387 | if ($predicate) 1388 | { 1389 | $can_serialize 1390 | } 1391 | else 1392 | { 1393 | DumpArrayObject $dump_state $contents 1394 | RegisterHelper $dump_state "CreatePSObject" 1395 | } 1396 | } 1397 | 1398 | 1399 | function CustomizeCollection($context, [Hashtable]$members_to_skip, [Hashtable]$members_to_add) 1400 | { 1401 | $members_to_skip["SyncRoot"] = 1 1402 | $members_to_add["Contents"] = @($context.o) 1403 | } 1404 | 1405 | 1406 | function CustomizeBitArray($context, [Hashtable]$members_to_skip, [Hashtable]$members_to_add) 1407 | { 1408 | $members_to_skip["SyncRoot"] = 1 1409 | $members_to_skip["Length"] = 1 1410 | $members_to_add["Contents"] = @($context.o) 1411 | } 1412 | 1413 | 1414 | function CustomizeSortedList($context, [Hashtable]$members_to_skip, [Hashtable]$members_to_add) 1415 | { 1416 | $members_to_skip["SyncRoot"] = 1 1417 | $members_to_skip["Keys"] = 1 1418 | $members_to_skip["Values"] = 1 1419 | $contents = @{} 1420 | foreach ($key in $context.o.Keys) 1421 | { 1422 | $contents[$key] = $context.o[$key] 1423 | } 1424 | $members_to_add["Contents"] = $contents 1425 | } 1426 | 1427 | 1428 | Initialize 1429 | 1430 | Export-ModuleMember -Function @('*') -Alias @('ctsb', 'dump') 1431 | -------------------------------------------------------------------------------- /DSC CI Pipeline Lab.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerShell/Demo_CI/fca2297f29d6958d0084dc9ccad81e47b16ea59f/DSC CI Pipeline Lab.docx -------------------------------------------------------------------------------- /InfraDNS/Build.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module psake 3 | 4 | function Invoke-TestFailure 5 | { 6 | param( 7 | [parameter(Mandatory=$true)] 8 | [validateSet('Unit','Integration','Acceptance')] 9 | [string]$TestType, 10 | 11 | [parameter(Mandatory=$true)] 12 | $PesterResults 13 | ) 14 | 15 | $errorID = if($TestType -eq 'Unit'){'UnitTestFailure'}elseif($TestType -eq 'Integration'){'InetegrationTestFailure'}else{'AcceptanceTestFailure'} 16 | $errorCategory = [System.Management.Automation.ErrorCategory]::LimitsExceeded 17 | $errorMessage = "$TestType Test Failed: $($PesterResults.FailedCount) tests failed out of $($PesterResults.TotalCount) total test." 18 | $exception = New-Object -TypeName System.SystemException -ArgumentList $errorMessage 19 | $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,$errorID, $errorCategory, $null 20 | 21 | Write-Output "##vso[task.logissue type=error]$errorMessage" 22 | Throw $errorRecord 23 | } 24 | 25 | FormatTaskName "--------------- {0} ---------------" 26 | 27 | Properties { 28 | $TestsPath = "$PSScriptRoot\Tests" 29 | $TestResultsPath = "$TestsPath\Results" 30 | $ArtifactPath = "$Env:BUILD_ARTIFACTSTAGINGDIRECTORY" 31 | $ModuleArtifactPath = "$ArtifactPath\Modules" 32 | $MOFArtifactPath = "$ArtifactPath\MOF" 33 | $ConfigPath = "$PSScriptRoot\Configs" 34 | $RequiredModules = @(@{Name='xDnsServer';Version='1.7.0.0'}, @{Name='xNetworking';Version='2.9.0.0'}) 35 | } 36 | 37 | Task Default -depends UnitTests 38 | 39 | Task GenerateEnvironmentFiles -Depends Clean { 40 | Exec {& $PSScriptRoot\DevEnv.ps1 -OutputPath $ConfigPath} 41 | } 42 | 43 | Task InstallModules -Depends GenerateEnvironmentFiles { 44 | # Install resources on build agent 45 | "Installing required resources..." 46 | 47 | #Workaround for bug in Install-Module cmdlet 48 | if(!(Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction Ignore)) 49 | { 50 | Install-PackageProvider -Name NuGet -Force 51 | } 52 | 53 | if (!(Get-PSRepository -Name PSGallery -ErrorAction Ignore)) 54 | { 55 | Register-PSRepository -Name PSGallery -SourceLocation https://www.powershellgallery.com/api/v2/ -InstallationPolicy Trusted -PackageManagementProvider NuGet 56 | } 57 | 58 | #End Workaround 59 | 60 | foreach ($Resource in $RequiredModules) 61 | { 62 | Install-Module -Name $Resource.Name -RequiredVersion $Resource.Version -Repository 'PSGallery' -Force 63 | Save-Module -Name $Resource.Name -RequiredVersion $Resource.Version -Repository 'PSGallery' -Path $ModuleArtifactPath -Force 64 | } 65 | } 66 | 67 | Task ScriptAnalysis -Depends InstallModules { 68 | # Run Script Analyzer 69 | "Starting static analysis..." 70 | Invoke-ScriptAnalyzer -Path $ConfigPath 71 | 72 | } 73 | 74 | Task UnitTests -Depends ScriptAnalysis { 75 | # Run Unit Tests with Code Coverage 76 | "Starting unit tests..." 77 | 78 | $PesterResults = Invoke-Pester -path "$TestsPath\Unit\" -CodeCoverage "$ConfigPath\*.ps1" -OutputFile "$TestResultsPath\UnitTest.xml" -OutputFormat NUnitXml -PassThru 79 | 80 | if($PesterResults.FailedCount) #If Pester fails any tests fail this task 81 | { 82 | Invoke-TestFailure -TestType Unit -PesterResults $PesterResults 83 | } 84 | 85 | } 86 | 87 | Task CompileConfigs -Depends UnitTests, ScriptAnalysis { 88 | # Compile Configurations... 89 | "Starting to compile configuration..." 90 | . "$ConfigPath\DNSServer.ps1" 91 | 92 | DNSServer -ConfigurationData "$ConfigPath\DevEnv.psd1" -OutputPath "$MOFArtifactPath\DevEnv\" 93 | # Build steps for other environments can follow here. 94 | } 95 | 96 | Task Clean { 97 | "Starting Cleaning enviroment..." 98 | #Make sure output path exist for MOF and Module artifiacts 99 | New-Item $ModuleArtifactPath -ItemType Directory -Force 100 | New-Item $MOFArtifactPath -ItemType Directory -Force 101 | 102 | # No need to delete Artifacts as VS does it automatically for each build 103 | 104 | #Remove Test Results from previous runs 105 | New-Item $TestResultsPath -ItemType Directory -Force 106 | Remove-Item "$TestResultsPath\*.xml" -Verbose 107 | 108 | #Remove ConfigData generated from previous runs 109 | Remove-Item "$ConfigPath\*.psd1" -Verbose 110 | 111 | #Remove modules that were installed on build Agent 112 | foreach ($Resource in $RequiredModules) 113 | { 114 | $Module = Get-Module -Name $Resource.Name 115 | if($Module -And $Module.Version.ToString() -eq $Resource.Version) 116 | { 117 | Uninstall-Module -Name $Resource.Name -RequiredVersion $Resource.Version 118 | } 119 | } 120 | 121 | $Error.Clear() 122 | } 123 | -------------------------------------------------------------------------------- /InfraDNS/Configs/DNSServer.ps1: -------------------------------------------------------------------------------- 1 | 2 | configuration DNSServer 3 | { 4 | Import-DscResource -module 'xDnsServer','xNetworking', 'PSDesiredStateConfiguration' 5 | 6 | Node $AllNodes.Where{$_.Role -eq 'DNSServer'}.NodeName 7 | { 8 | WindowsFeature DNS 9 | { 10 | Ensure = 'Present' 11 | Name = 'DNS' 12 | } 13 | 14 | xDnsServerPrimaryZone $Node.zone 15 | { 16 | Ensure = 'Present' 17 | Name = $Node.Zone 18 | DependsOn = '[WindowsFeature]DNS' 19 | } 20 | 21 | foreach ($ARec in $Node.ARecords.keys) { 22 | xDnsRecord $ARec 23 | { 24 | Ensure = 'Present' 25 | Name = $ARec 26 | Zone = $Node.Zone 27 | Type = 'ARecord' 28 | Target = $Node.ARecords[$ARec] 29 | DependsOn = '[WindowsFeature]DNS' 30 | } 31 | } 32 | 33 | foreach ($CName in $Node.CNameRecords.keys) { 34 | xDnsRecord $CName 35 | { 36 | Ensure = 'Present' 37 | Name = $CName 38 | Zone = $Node.Zone 39 | Type = 'CName' 40 | Target = $Node.CNameRecords[$CName] 41 | DependsOn = '[WindowsFeature]DNS' 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /InfraDNS/Deploy.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module psake 3 | 4 | function Invoke-TestFailure 5 | { 6 | param( 7 | [parameter(Mandatory=$true)] 8 | [validateSet('Unit','Integration','Acceptance')] 9 | [string]$TestType, 10 | 11 | [parameter(Mandatory=$true)] 12 | $PesterResults 13 | ) 14 | 15 | $errorID = if($TestType -eq 'Unit'){'UnitTestFailure'}elseif($TestType -eq 'Integration'){'InetegrationTestFailure'}else{'AcceptanceTestFailure'} 16 | $errorCategory = [System.Management.Automation.ErrorCategory]::LimitsExceeded 17 | $errorMessage = "$TestType Test Failed: $($PesterResults.FailedCount) tests failed out of $($PesterResults.TotalCount) total test." 18 | $exception = New-Object -TypeName System.SystemException -ArgumentList $errorMessage 19 | $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,$errorID, $errorCategory, $null 20 | 21 | Write-Output "##vso[task.logissue type=error]$errorMessage" 22 | Throw $errorRecord 23 | } 24 | 25 | FormatTaskName "--------------- {0} ---------------" 26 | 27 | Properties { 28 | $TestsPath = "$PSScriptRoot\Tests" 29 | $TestResultsPath = "$TestsPath\Results" 30 | $ArtifactPath = "$PSScriptRoot\.." 31 | $ModuleArtifactPath = "$ArtifactPath\Modules" 32 | $MOFArtifactPath = "$ArtifactPath\MOF" 33 | } 34 | 35 | Task Default -depends AcceptanceTests, IntegrationTests 36 | 37 | Task DeployModules -Depends Clean { 38 | # Copy resources from build agent to target node(s) 39 | # This task uses push to deploy resource modules to target nodes. This task could be used to package up and deploy resources to DSC pull server instead. 40 | "Deploying resources to target nodes..." 41 | 42 | $Session = New-PSSession -ComputerName TestAgent1 43 | 44 | $ModulePath = "$env:ProgramFiles\WindowsPowerShell\Modules\" 45 | $ModuleArtifacts = "$ModuleArtifactPath" 46 | 47 | copy-item $ModuleArtifacts $ModulePath -Recurse -Force -ToSession $Session 48 | 49 | Remove-PSSession $Session 50 | } 51 | 52 | Task DeployConfigs -Depends DeployModules { 53 | "Deploying configurations to target nodes..." 54 | #This task uses push to deploy configurations. This task could be used to package up and push configurations to pull server instead. 55 | Start-DscConfiguration -path "$MOFArtifactPath\DevEnv" -Wait -Verbose 56 | } 57 | 58 | Task IntegrationTests -Depends DeployConfigs { 59 | "Starting Integration tests..." 60 | #Run Integration tests on target node 61 | $Session = New-PSSession -ComputerName TestAgent1 62 | 63 | #Create a folder to store test script on remote node 64 | Invoke-Command -Session $Session -ScriptBlock { $null = new-item \Tests\ -ItemType Directory -Force } 65 | Copy-Item -Path "$TestsPath\Integration\*" -Destination "c:\Tests" -ToSession $Session -verbose 66 | 67 | #Run pester on remote node and collect results 68 | $PesterResults = Invoke-Command -Session $Session -ScriptBlock { Invoke-Pester -Path c:\Tests -OutputFile "c:\Tests\IntegrationTest.xml" -OutputFormat NUnitXml -PassThru } 69 | 70 | #Get Results xml from remote node 71 | Copy-Item -path "c:\Tests\IntegrationTest.xml" -Destination "$TestResultsPath" -FromSession $Session #-ErrorAction Continue 72 | Invoke-Command -Session $Session -ScriptBlock {remove-Item "c:\Tests\" -Recurse} #-ErrorAction Continue 73 | 74 | if($PesterResults.FailedCount) #If Pester fails any tests fail this task 75 | { 76 | Invoke-TestFailure -TestType Integration -PesterResults $PesterResults 77 | } 78 | 79 | Remove-PSSession $Session 80 | } 81 | 82 | Task AcceptanceTests -Depends DeployConfigs, IntegrationTests { 83 | "Starting Acceptance tests..." 84 | #Set module path 85 | 86 | $PesterResults = Invoke-Pester -path "$TestsPath\Acceptance\" -OutputFile "$TestResultsPath\AcceptanceTest.xml" -OutputFormat NUnitXml -PassThru 87 | 88 | if($PesterResults.FailedCount) #If Pester fails any tests fail this task 89 | { 90 | Invoke-TestFailure -TestType Acceptance -PesterResults $PesterResults 91 | } 92 | } 93 | 94 | Task Clean { 95 | "Starting Cleaning enviroment..." 96 | try { 97 | #Make sure Test Result location exists 98 | New-Item $TestResultsPath -ItemType Directory -Force 99 | 100 | #Remove modules from target node 101 | $Session = New-PSSession -ComputerName TestAgent1 102 | $RequiredModules = @() 103 | dir $ModuleArtifactPath -Directory | %{$RequiredModules += @{Name="$($_.Name)";Version="$(dir $ModuleArtifactPath\$($_.Name))"}} 104 | 105 | foreach ($Resource in $RequiredModules) 106 | { 107 | $ModulePath = "$env:ProgramFiles\WindowsPowerShell\Modules\$($Resource.Name)\$($Resource.Version)\" 108 | 109 | Invoke-Command -ScriptBlock {if(Test-Path $using:ModulePath) {Remove-Item $using:ModulePath -Recurse -Force}} -Session $Session 110 | 111 | } 112 | } 113 | finally 114 | { 115 | Remove-PSSession $Session -ErrorAction Ignore 116 | } 117 | 118 | 119 | } 120 | -------------------------------------------------------------------------------- /InfraDNS/DevEnv.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter(Mandatory=$true)] 3 | [string] 4 | $OutputPath 5 | ) 6 | 7 | Import-Module $PSScriptRoot\..\Assets\DscPipelineTools\DscPipelineTools.psd1 -Force 8 | 9 | # Define Unit Test Environment 10 | $DevEnvironment = @{ 11 | Name = 'DevEnv'; 12 | Roles = @( 13 | @{ Role = 'DNSServer'; 14 | VMName = 'TestAgent1'; 15 | Zone = 'Contoso.com'; 16 | ARecords = @{'TFSSrv1'= '10.0.0.10';'Client'='10.0.0.15';'BuildAgent'='10.0.0.30';'TestAgent1'='10.0.0.40';'TestAgent2'='10.0.0.50'}; 17 | CNameRecords = @{'DNS' = 'TestAgent1.contoso.com'}; 18 | } 19 | ) 20 | } 21 | 22 | Return New-DscConfigurationDataDocument -RawEnvData $DevEnvironment -OutputPath $OutputPath -------------------------------------------------------------------------------- /InfraDNS/Tests/Acceptance/DNSServer.tests.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Acceptance tests for DNS server Configuration 3 | # 4 | # Acceptance tests: DNS server is configured as intended. 5 | #################################################################### 6 | 7 | Import-Module PoshSpec 8 | Clear-DnsClientCache 9 | 10 | Describe 'Web Server E2E' { 11 | Context 'DNS addressess' { 12 | It "Should resolve TestAgent1 to 10.0.0.40" { 13 | (Resolve-DnsName -Name testagent1.contoso.com -DnsOnly -NoHostsFile).IPAddress | Should be '10.0.0.40' 14 | } 15 | 16 | It "Should resolve TestAgent2 to 10.0.0.50" { 17 | (Resolve-DnsName -Name testagent2.contoso.com -DnsOnly -NoHostsFile).IPAddress | Should be '10.0.0.50' 18 | } 19 | 20 | It "Should resolve DNS to TestAgent1" { 21 | (Resolve-DnsName -Name dns.contoso.com -Type CNAME -DnsOnly -NoHostsFile).NameHost | Should match 'TestAgent1' 22 | } 23 | } 24 | 25 | Context 'Web server ports' { 26 | 27 | $PortTest = Test-NetConnection -ComputerName testagent2.contoso.com -Port 80 28 | 29 | It "Should successfully Test TCP port 80" { 30 | $PortTest.TcpTestSucceeded | Should be $true 31 | } 32 | 33 | It "Should not be able to ping port 80" { 34 | $PortTest.PingSucceeded | Should be $false 35 | } 36 | } 37 | 38 | Context 'Website content' { 39 | $WebRequest = Invoke-WebRequest -Uri http://testagent2.contoso.com -UseBasicParsing 40 | 41 | It "Should have a status code of 200" { 42 | $WebRequest.StatusCode | Should be 200 43 | } 44 | 45 | It "Should have appropriate headers" { 46 | $WebRequest.Headers.Server | Should Match 'Microsoft-IIS/10.0' 47 | } 48 | 49 | It "Should have expected raw content length" { 50 | $WebRequest.RawContentLength | Should be 36919 51 | } 52 | 53 | It "Should have expected raw content" { 54 | $WebRequest.Content | Should Match 'Pixel perfect design, created with love' 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /InfraDNS/Tests/Integration/DNSServer.tests.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Integration tests for DNSServer Config 3 | # 4 | # Integration tests: DNS server is configured as intended. 5 | #################################################################### 6 | 7 | Import-Module PoshSpec 8 | 9 | Describe 'DNS Server' { 10 | context "Installed and running" { 11 | Service DNS Status { Should Be Running } 12 | It "Should have feature installed" { 13 | (Get-WindowsFeature -Name DNS).InstallState | Should be 'Installed' 14 | } 15 | } 16 | 17 | context 'Inbound DNS rules' { 18 | $InboundRules = (Get-NetFirewallRule -DisplayGroup "DNS Service" | where {$_.Direction -eq 'Inbound'}) 19 | 20 | It "Should have 4 rules" { 21 | $InboundRules.count | Should be 4 22 | } 23 | 24 | It "All rules should be enabled" { 25 | $InboundRules | %{if(!($_.enabled -eq $true)){"Failed!"}} | Should be $null 26 | } 27 | 28 | It "All rules should not block traffic" { 29 | $InboundRules | %{if(!($_.Action -eq "Allow")){"Failed!"}} | Should be $null 30 | } 31 | 32 | It "Should be a rule for local TCP port 53" { 33 | ($InboundRules | Get-NetFirewallPortFilter | ?{$_.Protocol -eq "TCP" -and $_.LocalPort -eq 53}).LocalPort | Should match 53 34 | } 35 | 36 | It "Should be a rule for local UDP port 53" { 37 | ($InboundRules | Get-NetFirewallPortFilter | ?{$_.Protocol -eq "UDP" -and $_.LocalPort -eq 53}).LocalPort | Should match 53 38 | } 39 | } 40 | 41 | context 'Outbound DNS rules' { 42 | $OutboundRules = (Get-NetFirewallRule -DisplayGroup "DNS Service" | where {$_.Direction -eq 'Outbound'}) 43 | 44 | It "Should have 2 rules" { 45 | $OutboundRules.count | should be 2 46 | } 47 | 48 | It "All rules should be enabled" { 49 | $OutboundRules | %{if(!($_.enabled -eq $true)){"Failed!"}} | Should be $null 50 | } 51 | 52 | It "All rules should not block traffic" { 53 | $OutboundRules | %{if(!($_.Action -eq "Allow")){"Failed!"}} | Should be $null 54 | } 55 | 56 | It "Should be a rule for TCP on any port" { 57 | ($OutboundRules | Get-NetFirewallPortFilter | ?{$_.Protocol -eq "TCP"}).LocalPort | Should be 'Any' 58 | } 59 | 60 | It "Should be a rule for UDP on any port" { 61 | ($OutboundRules | Get-NetFirewallPortFilter | ?{$_.Protocol -eq "UDP"}).LocalPort | Should be 'Any' 62 | } 63 | 64 | } 65 | 66 | context 'DNS records' { 67 | It "Should have an A record for TestAgent1 (DNS Server)" { 68 | (Get-DnsServerResourceRecord -Name TestAgent1 -ZoneName contoso.com).RecordData.Ipv4Address.IpAddressToString | Should be '10.0.0.40' 69 | } 70 | 71 | It "Should have an A record for TestAgent2 (Web Server)" { 72 | (Get-DnsServerResourceRecord -Name TestAgent2 -ZoneName contoso.com).RecordData.Ipv4Address.IpAddressToString | Should be '10.0.0.50' 73 | } 74 | 75 | It "Should have a CName record for DNS pointing to TestAgent1" { 76 | (Get-DnsServerResourceRecord -Name dns -ZoneName contoso.com -ErrorAction SilentlyContinue).RecordData.HostNameAlias | Should match 'TestAgent1.' 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /InfraDNS/Tests/Results/Readme.md: -------------------------------------------------------------------------------- 1 | #Test Results 2 | Store test results in this directory -------------------------------------------------------------------------------- /InfraDNS/Tests/Unit/DNSServer.tests.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Unit tests for DNSServer 3 | # 4 | # Unit tests content of DSC configuration as well as the MOF output. 5 | #################################################################### 6 | 7 | #region 8 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 9 | Write-Verbose $here 10 | $parent = Split-Path -Parent $here 11 | $GrandParent = Split-Path -Parent $parent 12 | Write-Verbose $GrandParent 13 | $configPath = Join-Path $GrandParent "Configs" 14 | Write-Verbose $configPath 15 | $sut = ($MyInvocation.MyCommand.ToString()) -replace ".Tests.","." 16 | Write-Verbose $sut 17 | . $(Join-Path -Path $configPath -ChildPath $sut) 18 | 19 | #endregion 20 | 21 | Describe "DNSServer Configuration" { 22 | Context "Configuration Script"{ 23 | 24 | It "Should be a DSC configuration script" { 25 | (Get-Command DNSServer).CommandType | Should be "Configuration" 26 | } 27 | 28 | It "Should not be a DSC Meta-configuration" { 29 | (Get-Command DNSServer).IsMetaConfiguration | Should Not be $true 30 | } 31 | 32 | It "Should use the xDNSServer DSC resource" { 33 | (Get-Command DNSServer).Definition | Should Match "xDNSServer" 34 | } 35 | } 36 | 37 | Context "Node Configuration" { 38 | $OutputPath = "TestDrive:\" 39 | 40 | It "Should not be null" { 41 | "$configPath\DevEnv.psd1" | Should Exist 42 | } 43 | 44 | It "Should generate a single mof file." { 45 | DNSServer -ConfigurationData "$configPath\DevEnv.psd1" -OutputPath $OutputPath 46 | (Get-ChildItem -Path $OutputPath -File -Filter "*.mof" -Recurse ).count | Should be 1 47 | } 48 | 49 | It "Should generate a mof file with the name 'TestAgent1'." { 50 | DNSServer -ConfigurationData "$configPath\DevEnv.psd1" -OutputPath $OutputPath 51 | Join-Path $OutputPath "TestAgent1.mof" | Should Exist 52 | } 53 | 54 | It "Should generate a new version (2.0) mof document." { 55 | DNSServer -ConfigurationData "$configPath\DevEnv.psd1" -OutputPath $OutputPath 56 | Join-Path $OutputPath "TestAgent1.mof" | Should Contain "Version=`"2.0.0`"" 57 | } 58 | 59 | #Clean up TestDrive between each test 60 | AfterEach { 61 | Remove-Item TestDrive:\* -Recurse 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Initiate.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter()] 3 | [ValidateSet('Build','Deploy')] 4 | [string] 5 | $fileName 6 | ) 7 | 8 | #$Error.Clear() 9 | 10 | Invoke-PSake $PSScriptRoot\InfraDNS\$fileName.ps1 11 | 12 | <#if($Error.count) 13 | { 14 | Throw "$fileName script failed. Check logs for failure details." 15 | } 16 | #> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sample Continuous integration Pipeline project 2 | 3 | This content will be used for labs showing how to create a CI Pipeline useing DSC, Pester, Visual Studio TFS, etc. 4 | -------------------------------------------------------------------------------- /WebApp/Configs/WebsiteConfig.ps1: -------------------------------------------------------------------------------- 1 | Configuration Website 2 | { 3 | import-dscresource -module PsDesiredStateConfiguration 4 | 5 | node $AllNodes.where{$_.Role -eq "Website"}.NodeName 6 | { 7 | Environment Type 8 | { 9 | ensure = 'Present' 10 | Name = 'TypeOfServer' 11 | Value = 'Web' 12 | } 13 | } 14 | } 15 | 16 | Website -OutputPath c:\Configs\ 17 | 18 | <# Removing config temporarily 19 | Configuration Website 20 | { 21 | param 22 | ( 23 | # Source Path for Website content 24 | [Parameter(Mandatory)] 25 | [ValidateNotNullOrEmpty()] 26 | [String]$SourcePath, 27 | 28 | # Name of the website to create 29 | [Parameter()] 30 | [String]$WebSiteName = 'FourthCoffee', 31 | 32 | # Destination path for Website content 33 | [Parameter()] 34 | [String]$DestinationRootPath = 'c:\inetpub\' 35 | ) 36 | 37 | # Import the module that defines custom resources 38 | Import-DscResource -Module xWebAdministration 39 | Import-DscResource -ModuleName PSDesiredStateConfiguration 40 | 41 | Node "Website.$WebSiteName" 42 | { 43 | # Install the IIS role 44 | WindowsFeature IIS 45 | { 46 | Ensure = "Present" 47 | Name = "Web-Server" 48 | } 49 | 50 | # Install the ASP .NET 4.5 role 51 | WindowsFeature AspNet45 52 | { 53 | Ensure = "Present" 54 | Name = "Web-Asp-Net45" 55 | } 56 | 57 | # Stop the default website 58 | xWebsite DefaultSite 59 | { 60 | Ensure = "Present" 61 | Name = "Default Web Site" 62 | State = "Stopped" 63 | PhysicalPath = "C:\inetpub\wwwroot" 64 | DependsOn = "[WindowsFeature]IIS" 65 | } 66 | 67 | # Copy the website content 68 | Archive WebContent 69 | { 70 | Ensure = "Present" 71 | Path = "$SourcePath\BakeryWebsite.zip" 72 | Destination = $DestinationPath 73 | DependsOn = "[WindowsFeature]AspNet45" 74 | } 75 | 76 | # Create the new Website 77 | xWebsite BakeryWebSite 78 | { 79 | Ensure = "Present" 80 | Name = $WebSiteName 81 | State = "Started" 82 | PhysicalPath = "$DestinationPath\BakeryWebsite" 83 | DependsOn = "[Archive]WebContent" 84 | } 85 | } 86 | } 87 | #> -------------------------------------------------------------------------------- /WebApp/Deploy.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module psake 3 | Import-Module $PSScriptRoot\..\Assets\DscPipelineTools\DscPipelineTools.psd1 -Force 4 | 5 | FormatTaskName "--------------- {0} ---------------" 6 | 7 | Properties { 8 | $TestsPath = Join-Path $PSScriptRoot 'Tests' 9 | $TestResultsPath = Join-Path $TestsPath 'Results' 10 | $ArtifactPath = Join-Path $PSScriptRoot '..' 11 | $ModuleArtifactPath = Join-Path $ArtifactPath 'Modules' 12 | $MOFArtifactPath = Join-Path $ArtifactPath 'MOF' 13 | } 14 | 15 | Task Default -depends AcceptanceTests 16 | 17 | Task DeployModules -Depends Clean { 18 | # Copy resources from build agent to target node(s) 19 | # This task uses push to deploy resource modules to target nodes. This task could be used to package up and deploy resources to DSC pull server instead. 20 | 'Deploying resources to target nodes...' 21 | 22 | $Session = New-PSSession -ComputerName TestAgent1 23 | 24 | $ModulePath = "$(Join-Path $env:ProgramFiles 'WindowsPowerShell\Modules')" 25 | $ModuleArtifacts = "$ModuleArtifactPath" 26 | 27 | copy-item $ModuleArtifacts $ModulePath -Recurse -Force -ToSession $Session 28 | 29 | Remove-PSSession $Session 30 | } 31 | 32 | Task DeployConfigs -Depends DeployModules { 33 | 'Deploying configurations to target nodes...' 34 | #This task uses push to deploy configurations. This task could be used to package up and push configurations to pull server instead. 35 | Start-DscConfiguration -path "$(Join-Path $MOFArtifactPath 'DevEnv')" -Wait -Verbose 36 | } 37 | 38 | Task IntegrationTests -Depends DeployConfigs { 39 | 'Starting Integration tests...' 40 | #Run Integration tests on target node 41 | $Session = New-PSSession -ComputerName TestAgent1 42 | 43 | #Create a folder to store test script on remote node 44 | Invoke-Command -Session $Session -ScriptBlock { $null = new-item \Tests\ -ItemType Directory -Force } 45 | Copy-Item -Path "$(Join-Path $TestsPath 'Integration\*')" -Destination 'c:\Tests' -ToSession $Session -verbose 46 | 47 | #Run pester on remote node and collect results 48 | $PesterResults = Invoke-Command -Session $Session ` 49 | -ScriptBlock { Invoke-Pester -Path 'c:\Tests' -OutputFile 'c:\Tests\IntegrationTest.xml' -OutputFormat NUnitXml -PassThru } 50 | 51 | #Get Results xml from remote node 52 | Copy-Item -path 'c:\Tests\IntegrationTest.xml' -Destination "$TestResultsPath" -FromSession $Session 53 | Invoke-Command -Session $Session -ScriptBlock {remove-Item 'c:\Tests\' -Recurse} 54 | 55 | New-TestValidation -TestType Integration -PesterResults $PesterResults 56 | 57 | Remove-PSSession $Session 58 | } 59 | 60 | Task AcceptanceTests -Depends DeployConfigs, IntegrationTests { 61 | 'Starting Acceptance tests...' 62 | #Set module path 63 | 64 | $PesterResults = Invoke-Pester -path "$(Join-Path $TestsPath 'Acceptance')" ` 65 | -OutputFile "$(Join-Path $TestResultsPath 'AcceptanceTest.xml')" ` 66 | -OutputFormat NUnitXml ` 67 | -PassThru 68 | 69 | 70 | New-TestValidation -TestType Acceptance -PesterResults $PesterResults 71 | 72 | } 73 | 74 | Task Clean { 75 | 'Starting Cleaning enviroment...' 76 | try { 77 | #Make sure Test Result location exists 78 | New-Item $TestResultsPath -ItemType Directory -Force 79 | 80 | #Remove modules from target node 81 | $Session = New-PSSession -ComputerName TestAgent1 82 | $RequiredModules = @() 83 | dir $ModuleArtifactPath -Directory | %{$RequiredModules += ` 84 | @{Name="$($_.Name)";Version="$(dir $ModuleArtifactPath\$($_.Name))"}} 85 | 86 | foreach ($Resource in $RequiredModules) 87 | { 88 | $ModulePath = "Join-Path $env:ProgramFiles 'WindowsPowerShell\Modules'" 89 | $ModulePath = "Join-Path $ModulePath $($Resource.Name)" 90 | $ModulePath = "Join-Path $ModulePath $($Resource.Version)" 91 | 92 | Invoke-Command -ScriptBlock {if(Test-Path $using:ModulePath) {Remove-Item $using:ModulePath -Recurse -Force}} ` 93 | -Session $Session 94 | } 95 | } 96 | finally 97 | { 98 | Remove-PSSession $Session -ErrorAction Ignore 99 | } 100 | 101 | 102 | } -------------------------------------------------------------------------------- /WebApp/DevEnv.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter(Mandatory=$true)] 3 | [string] 4 | $OutputPath 5 | ) 6 | 7 | Import-Module $PSScriptRoot\..\Assets\DscPipelineTools\DscPipelineTools.psd1 -Force 8 | 9 | # Define Unit Test Environment 10 | $DevEnvironment = @{ 11 | Name = 'DevEnv' 12 | Roles = @( 13 | @{ Role = 'WebServer' 14 | VMName = 'TestAgent1' 15 | Zone = 'Contoso.com' 16 | } 17 | ) 18 | } 19 | 20 | New-DscConfigurationDataDocument -RawEnvData $DevEnvironment -OutputPath $OutputPath -------------------------------------------------------------------------------- /WebApp/TestEnv.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module $PSScriptRoot\..\Assets\DscPipelineTools\DscPipelineTools.psd1 -Force 3 | 4 | 5 | # Define Unit Test Environment 6 | $UnitTestEnvironment = @{ 7 | Name = 'Test'; 8 | Roles = @( 9 | @{ Role = 'Website'; 10 | VMName = 'TestAgent2'; 11 | }, 12 | @{ Role = 'DNSServer'; 13 | VMName = 'TestAgent1'; 14 | } 15 | ) 16 | } 17 | 18 | Return New-DscConfigurationDataDocument -RawEnvData $UnitTestEnvironment -OutputPath $PSScriptRoot\Configs -------------------------------------------------------------------------------- /WebApp/Tests/Acceptance/Website.test.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Acceptance tests for WebsiteConfig 3 | # 4 | # Acceptance tests: Website is configured as intended. 5 | #################################################################### 6 | 7 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 8 | Write-Verbose $here 9 | $parent = Split-Path -Parent $here 10 | Write-Verbose $parent 11 | $configPath = Join-Path $parent "Configs" 12 | Write-Verbose $configPath 13 | $sut = ($MyInvocation.MyCommand.ToString()) -Replace ".Tests.", "." 14 | Write-Verbose $sut 15 | . $(Join-Path $configPath $sut) 16 | 17 | if (! (Get-Module xWebAdministration -ListAvailable)) 18 | { 19 | Install-Module -Name xWebAdministration -Force 20 | } 21 | 22 | Describe "HTTP" { 23 | TCPPort TestAgent2 80 PingSucceeded {Should Be $true} 24 | Http http:\\TestAgent2 StatusCode { Should Be 200} 25 | } -------------------------------------------------------------------------------- /WebApp/Tests/Integration/Website.test.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Integration tests for WebsiteConfig 3 | # 4 | # Integration tests: Website is configured as intended. 5 | #################################################################### 6 | 7 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 8 | Write-Verbose $here 9 | $parent = Split-Path -Parent $here 10 | Write-Verbose $parent 11 | $configPath = Join-Path $parent "Configs" 12 | Write-Verbose $configPath 13 | $sut = ($MyInvocation.MyCommand.ToString()) -Replace ".Tests.", "." 14 | Write-Verbose $sut 15 | . $(Join-Path $configPath $sut) 16 | 17 | if (! (Get-Module xWebAdministration -ListAvailable)) 18 | { 19 | Install-Module -Name xWebAdministration -Force 20 | } 21 | 22 | Describe "Website configuration" { 23 | It Should "Initial do nothing test." { 24 | $true | should be $true 25 | } 26 | } -------------------------------------------------------------------------------- /WebApp/Tests/Unit/Website.test.ps1: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Unit tests for WebsiteConfig 3 | # 4 | # Unit tests content of DSC configuration as well as the MOF output. 5 | #################################################################### 6 | 7 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 8 | Write-Verbose $here 9 | $parent = Split-Path -Parent $here 10 | Write-Verbose $parent 11 | $configPath = Join-Path $parent "Configs" 12 | Write-Verbose $configPath 13 | $sut = ($MyInvocation.MyCommand.ToString()) -Replace ".Tests.", "." 14 | Write-Verbose $sut 15 | . $(Join-Path $configPath $sut) 16 | 17 | if (! (Get-Module xWebAdministration -ListAvailable)) 18 | { 19 | Install-Module -Name xWebAdministration -Force 20 | } 21 | 22 | Describe "Website Configuration" { 23 | 24 | Context "Configuration Script"{ 25 | 26 | It "Should be a DSC configuration script" { 27 | (Get-Command Website).CommandType | Should be "Configuration" 28 | } 29 | 30 | It "Should not be a DSC Meta-configuration" { 31 | (Get-Command website).IsMetaConfiguration | Should Not be $true 32 | } 33 | 34 | It "Should require the source path parameter" { 35 | (Get-Command Website).Parameters["SourcePath"].Attributes.Mandatory | Should be $true 36 | } 37 | 38 | It "Should fail when an invalid source path is provided" { 39 | website -SourcePath "This is not a path" | should Throw 40 | } 41 | 42 | It "Should include the following 3 parameters: 'SourcePath','WebsiteName','DestinationRootPath' " { 43 | (Get-Command Website).Parameters["SourcePath","WebsiteName","DestinationRootPath"].ToString() | Should not BeNullOrEmpty 44 | } 45 | 46 | It "Should use the xWebsite DSC resource" { 47 | (Get-Command Website).Definition | Should Match "xWebsite" 48 | } 49 | } 50 | 51 | Context "Node Configuration" { 52 | $OutputPath = "TestDrive:\" 53 | 54 | It "Should generate a single mof file." { 55 | Website -OutputPath $OutputPath -SourcePath "\\Server1\Configs\" 56 | (Get-ChildItem -Path $OutputPath -File -Filter "*.mof" -Recurse ).count | Should be 1 57 | } 58 | 59 | It "Should generate a mof file with the name 'Website'." { 60 | Website -OutputPath $OutputPath -SourcePath "\\Server1\Configs\" 61 | Join-Path $OutputPath "Website.mof" | Should Exist 62 | } 63 | 64 | It "Should be a valid DSC MOF document"{ 65 | Website -OutputPath $OutputPath -SourcePath "\\Server1\Configs\" 66 | mofcomp -check "$OutputPath\Website.mof" | Select-String "compiler returned error" | Should BeNullOrEmpty 67 | } 68 | 69 | It "Should generate a new version (2.0) mof document." { 70 | Website -OutputPath $OutputPath -SourcePath "\\Server1\Configs\" 71 | Join-Path $OutputPath "Website.mof" | Should Contain "Version=`"2.0.0`"" 72 | } 73 | 74 | It "Should create a mof that has a website named 'BustersBuns'." { 75 | Website -OutputPath $OutputPath -SourcePath "\\Server1\Configs\" -WebSiteName "BustersBuns" 76 | Join-Path $OutputPath "Website.mof" | Should Contain "Name = `"BustersBuns`";" 77 | } 78 | 79 | #Clean up TestDrive between each test 80 | AfterEach { 81 | Remove-Item TestDrive:\* -Recurse 82 | } 83 | 84 | } 85 | } -------------------------------------------------------------------------------- /WebApp/build.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module psake 3 | 4 | Task Default -depends AcceptanceTests 5 | 6 | Task GenerateEnvironmentFiles{ 7 | Exec {& $PSScriptRoot\TestEnv.ps1} 8 | } 9 | 10 | Task ScriptAnalysis -Depends GenerateEnvironmentFiles { 11 | Import-Module PSScriptAnalyzer 12 | # Run Script Analyzer 13 | "Starting static analysis..." 14 | Invoke-ScriptAnalyzer -Path $PSScriptRoot\Configs 15 | } 16 | 17 | Task UnitTests -Depends ScriptAnalysis { 18 | 19 | # Run Unit Tests 20 | "Starting unit tests..." 21 | Import-Module Pester 22 | Push-Location $PSScriptRoot\Tests\Unit\ 23 | Invoke-Pester 24 | "Successfully completed unit tests." 25 | 26 | # Run Code Coverage to ensure it is greater than 75% 27 | "Starting code coverage..." 28 | $Coverage = Invoke-Pester -CodeCoverage 29 | #Fail if code coverage is less than 75% 30 | } 31 | 32 | Task CompileConfigs -Depends UnitTests { 33 | # Compile Configurations 34 | "Starting to compile configuration..." 35 | 36 | } 37 | 38 | Task IntegrationTests -Depends DeployConfigs, UnitTests { 39 | "Integration tests ran successfully" 40 | } 41 | 42 | Task DeployConfigs -Depends CompileConfigs { 43 | "Configs applied to target nodes" 44 | #push or Pull environment 45 | } 46 | 47 | Task AcceptanceTests -Depends DeployConfigs, IntegrationTests { 48 | "Acceptance tests processed" 49 | } --------------------------------------------------------------------------------