test/Automation/Invoke-RunTestScriptInDockerCoreContainers.ps1
<# .SYNOPSIS Automates PowerShell Core script testing on local Docker containers .DESCRIPTION Automates testing of PowerShell Core scripts on different operating systems by using local Docker containers running PowerShell Core images from the official Microsoft Docker hub. Performs these steps: - validates user-specified image names with local images and Docker hub versions; - for each valid Docker image name: - ensures container exists for this image, creating if necessary; - ensures container is running; - get temp folder path from container; - copies user's folders/files, including test script, from computer to container's temp folder; - runs test script in container; - stops container. If an error occurs running the test script in one container, all processing ceases after that container is stopped; no additional containers are tested as it's likely the test script would just fail on those as well. PowerShell Core Docker container test script written by Dan Ward. See https://github.com/DTW-DanWard/PowerShell-Beautifier or http://dtwconsulting.com for more information. I hope you enjoy using this utility! -Dan Ward .PARAMETER SourcePaths Folders and/or files on local machine to copy to container .PARAMETER TestFileAndParams Path to the test script with any params to run test; path is relative to SourcePaths; see example for more details .PARAMETER TestImageNames Docker image names to test against. Default values: 'ubuntu-16.04', 'centos-7' .PARAMETER DockerHubRepository Docker hub repository team/project name. Default value: "microsoft/powershell" .PARAMETER Quiet Run test with as little or no output possible. If all tests are successful on all containers, returns only $true. However if a container is specified in TestImageNames that does not exist locally, that info will be output. In addition, if an error occurs running a Docker command or running the test script, that info will also be output and $true will not be returned. .EXAMPLE .\Invoke-RunTestScriptInDockerCoreContainers.ps1 ` -SourcePaths 'C:\Path\To\PowerShell-Beautifier' ` -TestFileAndParams 'PowerShell-Beautifier/test/Invoke-PrettifyScriptTests.ps1 -Quiet' ` -TestImageNames ('ubuntu-16.04','centos-7') Key details here: - C:\Path\To\PowerShell-Beautifier is a folder that gets copied to each container. - The test script is located under that folder, so including that source folder name, the path is: PowerShell-Beautifier/test/Invoke-PrettifyScriptTests.ps1 - -Quiet is a parameter of Invoke-PrettifyScriptTests.ps1; when specified if no errors occur (knock on wood) only $true is returned. This script looks for $true to know the test on the current container was successful. - Tests with two images: microsoft/powershell:ubuntu-16.04 and microsoft/powershell:centos-7 .EXAMPLE .\Invoke-RunTestScriptInDockerCoreContainers.ps1 ` -SourcePaths ('c:\Code\Folder1','c:\Code\Folder2','c:\Code\TestFile.ps1') ` -TestFileAndParams 'TestFile.ps1' Key details here: - Multiple sources are being copied. - TestFile.ps1 is the test file to run here. - We are explicitly copying over TestFile.ps1, not a parent folder, so the script will be located in the root of the temp folder in the container. For that reason, there is no relative path to that script in the TestFileAndParams value. - That script could be anywhere, doesn't have to be in the root of c:\Code, so the SourcePath value could be c:\Code\TestScripts\Blahblah\TestFile.ps1 but the TestFileAndParams value would be the same. #> #region Script parameters # note: the default values below are specific to my machine and the PowerShell-Beautifier # project. I tried to parameterize and genericize this as much as possible so that it could # be used by others with *preferably* no code changes. See readme.md in same folder as this # script for more information about running this script. param( [string[]]$SourcePaths = @((Split-Path -Path (Split-Path -Path (Split-Path -Path $PSCommandPath -Parent) -Parent) -Parent)), [string]$TestFileAndParams = 'PowerShell-Beautifier/test/Invoke-PrettifyScriptTests.ps1 -Quiet', [string[]]$TestImageNames = @('ubuntu-16.04','centos-7'), [string]$DockerHubRepository = 'microsoft/powershell', [switch]$Quiet ) #endregion Set-StrictMode -Version 2 #region Output startup info if ($Quiet -eq $false) { Write-Output ' ' Write-Output 'Testing with these values:' Write-Output " Test file: $TestFileAndParams" Write-Output " Docker hub repo: $DockerHubRepository" Write-Output " Images names: $TestImageNames" if ($SourcePaths.Count -eq 1) { Write-Output " Source paths: $SourcePaths" } else { Write-Output ' Source paths:' $SourcePaths | ForEach-Object { Write-Output " $_" } } Write-Output ' ' } #endregion #region Misc functions #region Function: Out-ErrorInfo <# .SYNOPSIS Write-Output error information when running a command .DESCRIPTION Write-Output error information when running a command .PARAMETER Command Command that was run .PARAMETER Parameters Parameters for the command .PARAMETER ErrorInfo Error information captured to display .PARAMETER ErrorMessage Optional message to display before all error info .EXAMPLE Out-ErrorInfo -Command "docker" -Parameters "--notaparam" -ErrorInfo $CapturedError # Writes command, parameters and error info to output #> function Out-ErrorInfo { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Command, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [object[]]$Parameters, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [object[]]$ErrorInfo, [string]$ErrorMessage ) #endregion process { if ($ErrorMessage -ne $null -and $ErrorMessage.Trim() -ne '') { Write-Output $ErrorMessage } Write-Output 'Error occurred running this command:' Write-Output " $Command $Parameters" Write-Output 'Error info:' $ErrorInfo | ForEach-Object { Write-Output $_.ToString() } } } #endregion #region Function: Invoke-RunCommand <# .SYNOPSIS Runs 'legacy' command-line commands with call operator & .DESCRIPTION Runs 'legacy' command-line commands with call operator & in try/catch block and tests both $? and $LastExitCode for errors. If error occurs, writes out using Out-ErrorInfo. .PARAMETER Command Command to run .PARAMETER Parameters Parameters to use .PARAMETER ErrorMessage Optional message to display if error occurs .PARAMETER ExitOnError If error occurs, exit script .PARAMETER ErrorOccurred Return $true if error occurred, else false; only used by Copy-FilesToDockerContainer #> function Invoke-RunCommand { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Command, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [object[]]$Parameters, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ref]$Results, [string]$ErrorMessage, [switch]$ExitOnError, [ref]$ErrorOccurred ) #endregion process { try { if ($ErrorOccurred -ne $null) { $ErrorOccurred.Value = $false } $Results.Value = & $Command $Parameters 2>&1 if ($? -eq $false -or $LastExitCode -ne 0) { Out-ErrorInfo -Command $Command -Parameters $Parameters -ErrorInfo $Results.Value -ErrorMessage $ErrorMessage if ($ErrorOccurred -ne $null) { $ErrorOccurred.Value = $true } if ($ExitOnError -eq $true) { exit } } } catch { Out-ErrorInfo -Command $Command -Parameters $Parameters -ErrorInfo $_.Exception.Message -ErrorMessage $ErrorMessage if ($ErrorOccurred -ne $null) { $ErrorOccurred.Value = $true } if ($ExitOnError -eq $true) { exit } } } } #endregion #region Function: Confirm-ValidateUserImages <# .SYNOPSIS Validates script param TestImageNames entries: names, local availability, OS .DESCRIPTION Validates script parameter TestImageNames entries - checks name with locally installed images for repository DockerHubRepository - if found locally, also checks OS type for image matches current Docker server OS - if not found locally but is valid for repository DockerHubRepository (i.e. from the online hub data) outputs command for user to run to download image. If image is not found locally nor found at repository DockerHubRepository, writes error info but does not exit script (it will process any valid image names). .PARAMETER DockerHubRepositoryImageData Hashtable of valid image data from the Docker hub repository itself .PARAMETER ValidImageNames Reference parameter! Any/all valid image names found in list TestImageNames are returned in this parameter #> function Confirm-ValidateUserImages { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] $DockerHubRepositoryImageData, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ref]$ValidImageNames ) #endregion process { # get local images for Docker project $DockerHubRepository [object[]]$LocalDockerRepositoryImages = Get-DockerImageStatus # get list of local image names $LocalDockerRepositoryImageNames = $null if ($null -ne $LocalDockerRepositoryImages -and $LocalDockerRepositoryImages.Count -gt 0) { $LocalDockerRepositoryImageNames = $LocalDockerRepositoryImages.Tag } # get local Docker server OS [string]$DockerServerOS = Get-DockerServerOS $TestImageNames | ForEach-Object { $TestImageTagName = $_ if ($LocalDockerRepositoryImageNames -contains $TestImageTagName) { # make sure image OS is valid for current Docker server OS [string]$ImageOS = $DockerHubRepositoryImageData.$TestImageTagName.images.os if ($ImageOS -ne $DockerServerOS) { Write-Output ' ' Write-Output "Image $TestImageTagName cannot be tested at this time as the image OS type is $ImageOS" Write-Output "while your local Docker server OS is $DockerServerOS. You need to change your Docker" Write-Output 'server OS type; on Windows this can be done by right-clicking the Docker system' Write-Output "tray icon and selecting 'Change to $ImageOS containers'" Write-Output 'Note: if you do this there could be additional setup work if this is the first' Write-Output "time you are attempting to run $ImageOS containers on this machine." } else { $ValidImageNames.Value += $TestImageTagName } } else { # no need to check if .Keys doesn't exist; this data should always get pulled from Docker hub if ($DockerHubRepositoryImageData.Keys -contains $TestImageTagName) { #region Programming note # if the image name is valid but not installed locally we *could* just run the 'docker pull' command # ourselves programmatically. however, pulling down that much data (WindowsServerCore is 5GB!) is # really something the user should initiate. #endregion Write-Output "Image $TestImageTagName is not installed locally but exists in repository $DockerHubRepository" Write-Output 'To download and install type:' Write-Output (' docker pull ' + $DockerHubRepository + ':' + $TestImageTagName) Write-Output ' ' } else { Write-Output "Image $TestImageTagName is not installed locally and does not exist in repository $DockerHubRepository" Write-Output 'Do you have an incorrect image name? Valid image names are:' $DockerHubRepositoryImageData.Keys | Sort-Object | ForEach-Object { Write-Output " $_" } Write-Output ' ' } } } } } #endregion #region Function: Confirm-DockerHubRepositoryFormatCorrect <# .SYNOPSIS Confirms script param DockerHubRepository is <team name>/<project name> .DESCRIPTION Confirms script param DockerHubRepository is <team name>/<project name>, i.e. it should have only 1 slash in it 'in the middle' of other characters. If correct, does nothing, if incorrect writes info and exits script. #> function Confirm-DockerHubRepositoryFormatCorrect { process { # the value for $DockerHubRepository should be: <team name>/<project name> # i.e. it should have only 1 slash in it between other characters if ($DockerHubRepository -notmatch '^[^/]+/[^/]+$') { Write-Output "The format for DockerHubRepository is incorrect: $DockerHubRepository" Write-Output 'It should be in the format: TeamName/ProjectName' Write-Output 'That is: only 1 forward slash surrounded by other non-forward-slash text' exit } } } #endregion #region Function: Confirm-SourcePathsValid <# .SYNOPSIS Confirms script param SourcePaths paths all exist .DESCRIPTION Confirms script param SourcePaths paths all exist. If all paths exist, function does nothing; if they don't, error info is displayed and script exists. #> function Confirm-SourcePathsValid { process { $SourcePaths | ForEach-Object { $SourcePath = $_ if ($false -eq (Test-Path -Path $SourcePath)) { Write-Output "Source path not found: $SourcePath" exit } } } } #endregion #region Function: Convert-ImageDataToHashTables <# .SYNOPSIS Converts Docker hub project image/tags data to hashtable of hashtables .DESCRIPTION Converts Docker hub project image/tags data, in the form of an object array, to hashtable of hashtables for easier lookup. Also adds a sanitized / safe value to use for container name, based on repository:image name. .PARAMETER ImageDataPSObjects Image data as array of PSObjects #> function Convert-ImageDataToHashTables { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true,ValueFromPipeline = $false)] [ValidateNotNullOrEmpty()] [object[]]$ImageDataPSObjects ) #endregion process { $ImageDataHashTable = [ordered]@{} # for each entry in $ImageDataPSObjects: # create an entry in hash table $ImageDataHashTable # the key will be the image/tag name # the value will be a new hashtable containing the data from the PSObject plus # a new property ContainerName, which is a sanitized name to be used as the # Docker container name (which can only have certain characters) $ImageDataPSObjects.Name | Sort-Object | ForEach-Object { $Name = $_ $OneImageData = [ordered]@{} # get PSObject for this tag $TagObject = $ImageDataPSObjects | Where-Object { $_.Name -eq $Name } # for each property on the PSObject, add to hashtable ($TagObject | Get-Member -MemberType NoteProperty).Name | Sort-Object | ForEach-Object { $OneImageData.$_ = $TagObject.$_ } #region Container name information # when creating and using containers we want to use a specific container name; if you # don't specify a name, Docker will create the container with a random value. it's a lot # easier to find/start/use/stop a container with a distinct name you know in advance. # so we'll base the name on the Docker standard RepositoryName:ImageName; unfortunately # Docker's container name only allows certain characters (no slashes or colons) so we'll # add a sanitized ContainerName property to the image data in $ImageDataHashTable and use # that later in our code. # per Docker error message only these characters are valid for the --name parameter: # [a-zA-Z0-9][a-zA-Z0-9_.-] #endregion # replace any invalid characters with underscores to get sanitized/safe name $OneImageData.ContainerName = ($DockerHubRepository + '_' + $Name) -replace '[^a-z0-9_.-]','_' # now add this image/tag's hashtable data to the main $ImageDataHashTable hashtable $ImageDataHashTable.$Name = $OneImageData } #return data $ImageDataHashTable } } #endregion #region Function: Get-DockerHubProjectImageInfo <# .SYNOPSIS Returns Docker hub project image/tag info for $DockerHubRepository .DESCRIPTION Returns Docker hub project image/tag info for $DockerHubRepository; format is PSObjects. #> function Get-DockerHubProjectImageInfo { process { # path to tags for Docker project $ImageTagsUri = 'https://registry.hub.docker.com/v2/repositories/' + $DockerHubRepository + '/tags/?page=' [object[]]$Results = $null $Continue = $true $PageIndex = 1 while ($Continue) { $Uri = $ImageTagsUri + $PageIndex try { $Response = Invoke-WebRequest -Uri $Uri # Convert JSON response to PSObjects $Results += (ConvertFrom-Json -InputObject $Response.Content).results $PageIndex += 1 } catch { $Continue = $false } } $Results } } #endregion #endregion #region All Docker command functions #region Function: Confirm-DockerInstalled <# .SYNOPSIS Confirms Docker is installed .DESCRIPTION Confirms Docker is installed; if installed ('docker --version' works) then function does nothing. If not installed, reports error and exits script. #> function Confirm-DockerInstalled { process { $Cmd = 'docker' $Params = @('--version') $ErrorMessage = 'Docker does not appear to be installed or is not working correctly.' # capture Results output and discard; if error, Invoke-RunCommand exits script $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ErrorMessage $ErrorMessage -ExitOnError } } #endregion #region Function: Copy-FilesToDockerContainer <# .SYNOPSIS Copies $SourcePaths files to local container ContainerName .DESCRIPTION Copies all $SourcePaths files to local container ContainerName putting files under folder ContainerPath .PARAMETER ContainerName Name of container to copy files to. .PARAMETER ContainerPath Path in container to copy files to. .PARAMETER ErrorOccurred Return $true if error occurred, else false. .EXAMPLE Copy-FilesToDockerContainer -ContainerName MyContainer -ContainerPath /tmp # copies files from $SourcePaths local container named MyContainer under path ContainerPath #> function Copy-FilesToDockerContainer { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ref]$ErrorOccurred ) #endregion process { if ($Quiet -eq $false) { Write-Output " Copying source content to container temp folder $ContainerPath" } # for each source file path, copy to Docker container $SourcePaths | ForEach-Object { $SourcePath = $_ if ($Quiet -eq $false) { Write-Output " $SourcePath" } $Cmd = 'docker' $Params = @('cp',$SourcePath,($ContainerName + ':' + $ContainerPath)) # capture output, discard Results but return ErrorOccurred; don't exit on error $Results = $null $ErrorFound = $false Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ErrorOccurred ([ref]$ErrorFound) $ErrorOccurred.Value = $ErrorFound } } } #endregion #region Function: Initialize-DockerContainerAndStart <# .SYNOPSIS Creates local container and starts it .DESCRIPTION Creates local container and starts it using Docker run (as opposed to explicit docker create and start commands). Uses image $ImageName from repository $DockerHubRepository and creates with name $ContainerName. If error occurs, reports error and exits script. .PARAMETER ImageName Name of Docker image to use to create container. .PARAMETER ContainerName Name of container to create. .EXAMPLE Initialize-DockerContainerAndStart -ImageName MyImageName -ContainerName MyContainer # Creates local container from repository $DockerHubRepository using image MyImageName # naming it MyContainer and starts it. #> function Initialize-DockerContainerAndStart { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ImageName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName ) #endregion process { if ($Quiet -eq $false) { Write-Output ' Preexisting container not found; creating and starting' } $Cmd = 'docker' $Params = @('run','--name',$ContainerName,'-t','-d',($DockerHubRepository + ':' + $ImageName)) # capture output and discard; if error, Invoke-RunCommand exits script $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError } } #endregion #region Function: Get-DockerContainerTempFolderPath <# .SYNOPSIS Gets temp folder path in container ContainerName .DESCRIPTION Gets temp folder path inside running container ContainerName by running [System.IO.Path]::GetTempPath() If container is not running exists script with error. .PARAMETER ContainerName Name of container to create. .EXAMPLE Get-DockerContainerTempFolderPath -ContainerName microsoft_powershell_ubuntu-16.04 /tmp #> function Get-DockerContainerTempFolderPath { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName, [Parameter(Mandatory = $true)] [ref]$Path ) #endregion process { if ($Quiet -eq $false) { Write-Output ' Getting temp folder path in container' } # get container info for $ContainerName $ContainerInfo = Get-DockerContainerStatus | Where-Object { $_.Names -eq $ContainerName } # this error handling shouldn't be needed; at this point in the script # the container name has been validated and started, but just in case # if no container exists or container not started, exit with error if ($null -eq $ContainerInfo) { Write-Output "Container $ContainerName not found; exiting script" exit } elseif (!$ContainerInfo.Status.StartsWith('Up')) { Write-Output "Container $ContainerName isn't running but it should be; exiting script" exit } # Note: see developer info in Invoke-TestScriptInDockerContainer about why command is run # this particular way $Cmd = 'docker' $Params = @('exec',$ContainerName,'pwsh','-Command',"& { [System.IO.Path]::GetTempPath() }") # capture output and return; if error, Invoke-RunCommand exits script $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError $Path.Value = $Results } } #endregion #region Function: Get-DockerContainerStatus <# .SYNOPSIS Returns all local Docker container info as PSObjects .DESCRIPTION Returns all local Docker container info as PSObjects. If error occurs, reports error and exits script. .EXAMPLE Get-DockerContainerStatus | Format-Table # Additional content to right not shown Command CreatedAt ID Image ....... ------- --------- -- ----- ....... "powershell" 2017-09-07 12:50:43 -0400 EDT 6b0f74711cda microsoft/powershell:centos-7 ....... "powershell" 2017-09-07 12:46:03 -0400 EDT 7bec8cacd139 microsoft/powershell:ubuntu-16.04 ....... #> function Get-DockerContainerStatus { process { $Cmd = 'docker' $Params = @('ps','-a','--format','{{ json . }}') $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError # parse results, converting from JSON to PSObjects if ($null -ne $Results -and $Results.ToString().Trim() -ne '') { $Results | ConvertFrom-Json } else { $null } } } #endregion #region Function: Get-DockerImageStatus <# .SYNOPSIS Returns local Docker image info as PSObjects for repository $DockerHubRepository .DESCRIPTION Returns local Docker image info (images -a) as PSObjects. If error occurs, reports error and exits script. .EXAMPLE Get-DockerImageStatus | Format-Table # Additional content to right not shown Containers CreatedAt CreatedSince Digest ID Repository ....... ---------- --------- ------------ ------ -- ---------- ....... N/A 2017-08-31 15:45:18 -0400 EDT 7 days ago <none> c9a0ce9c00a0 microsoft/powershell ....... N/A 2017-08-31 15:45:11 -0400 EDT 7 days ago <none> e83ef70fc111 microsoft/powershell ....... N/A 2017-08-04 01:18:49 -0400 EDT 5 weeks ago <none> 61ae8d8940e6 microsoft/powershell ....... N/A 2017-06-14 15:29:01 -0400 EDT 2 months ago <none> 1815c82652c0 hello-world ....... #> function Get-DockerImageStatus { process { $Cmd = 'docker' $Params = @('images',$DockerHubRepository,'--format','{{json .}}') $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError # parse results, converting from JSON to PSObjects if ($null -ne $Results -and $Results.ToString().Trim() -ne '') { $Results | ConvertFrom-Json } else { $null } } } #endregion #region Function: Get-DockerServerOS <# .SYNOPSIS Returns local Docker server operating system: linux or windows .DESCRIPTION Returns local Docker server operating system: linux or windows. If error occurs, reports error and exits script. .EXAMPLE Get-DockerServerOS linux #> function Get-DockerServerOS { process { $Cmd = 'docker' $Params = @('info','--format','{{json .}}') $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError # parse results, converting from JSON to PSObject, then return OSType property ($Results | ConvertFrom-Json).OSType } } #endregion #region Function: Invoke-TestScriptInDockerContainer <# .SYNOPSIS Executes PowerShell script in local container .DESCRIPTION Executes script ScriptPath in container ContainerName; if error occurs, reports error and sets parameter TestScriptSuccess = $false (else $true). .PARAMETER ContainerName Name of container to use. .PARAMETER ScriptPath Path in container to run script. .PARAMETER TestScriptSuccess Reference parameter! $true if an error occurred running test .EXAMPLE Invoke-TestScriptInDockerContainer MyContainer /tmp/MyScript.ps1 ([ref]$TestScriptSuccess) # Executes script /tmp/MyScript.ps1 in container, sets $TestScriptSuccess = $false if error #> function Invoke-TestScriptInDockerContainer { #region Function parameters [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ScriptPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ref]$TestScriptSuccess ) #endregion process { if ($Quiet -eq $false) { Write-Output ' Running test script on container' } #region A handy tip # if you are reading this script, this next bit contains the biggest gotcha I encountered when # writing the Docker commands to run in PowerShell. if you were to type a Docker execute command in a # PowerShell window to execute a different PowerShell script in the container, it would look like this: # docker exec containername pwsh -Command { /SomeScript.ps1 } # There are multiple gotchas: # - On Windows: when converting this to a command with array of parameters to pass to the call # operator & (i.e.: & $Cmd $Params), you must explicitly create " /SomeScript.ps1 " as a # scriptblock first; passing it in as just a string will not execute no matter how you format it. # - But doing that fails on OSX native PowerShell Core! The trick to getting it to work is to use # *this* format of launching PowerShell with a command: # docker exec containername pwsh -Command "& { /SomeScript.ps1 }" #endregion $Cmd = 'docker' $Params = @('exec',$ContainerName,'pwsh','-Command',"& { $ScriptPath }") # capture output $Results; don't exit on error $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) # my test script in $TestFileAndParams when used with the -Quiet param, is designed to # return ONLY $true if everything worked. so if anything other than $true is returned assume # error and report results if ($null -ne $Results -and $Results -ne $true) { $TestScriptSuccess.Value = $false Out-ErrorInfo -Command $Cmd -Parameters $Params -ErrorInfo $Results } else { $TestScriptSuccess.Value = $true if ($Quiet -eq $false) { Write-Output ' Test script completed successfully' } } } } #endregion #region Function: Start-DockerContainer <# .SYNOPSIS Starts local container .DESCRIPTION Starts local container. If error occurs, reports error and exits script. .PARAMETER ContainerName Name of container to start. .EXAMPLE Start-DockerContainer MyContainer # starts local container named MyContainer #> function Start-DockerContainer { #region Function parameters [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName ) #endregion process { if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { if ($Quiet -eq $false) { Write-Output ' Starting container' } $Cmd = 'docker' $Params = @('start',$ContainerName) # capture output and discard; if error, Invoke-RunCommand exits script $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError } } } #endregion #region Function: Stop-DockerContainer <# .SYNOPSIS Stops local container .DESCRIPTION Stops local container. If error occurs, reports error and exits script. .PARAMETER ContainerName Name of container to stop. .EXAMPLE Stop-DockerContainer MyContainer # stops local container named MyContainer #> function Stop-DockerContainer { #region Function parameters [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ContainerName ) #endregion process { if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { if ($Quiet -eq $false) { Write-Output ' Stopping container' } $Cmd = 'docker' $Params = @('stop',$ContainerName) # capture output and discard; if error, Invoke-RunCommand exits script $Results = $null Invoke-RunCommand -Command $Cmd -Parameters $Params -Results ([ref]$Results) -ExitOnError } } } #endregion #endregion # ################ 'main' begins here # Programming note: to improve simplicity and readability, if any of the below functions # generates an error, info is written and the script is exited from within the function. # There are some exceptions: # if an error occurs in Copy-FilesToDockerContainer, ErrorOccurred is set to $true and # thus Invoke-TestScriptInDockerContainer will not be run (we can't execute the test # if code didn't copy correctly) # Invoke-TestScriptInDockerContainer does not exit the script, either, so processing # can continue and the container will be stopped (and *then* the script will exit). # make sure Docker is installed, 'docker' is in the path and is working # no point in continuing if Docker isn't working Confirm-DockerInstalled #region Get Docker hub image/tag data and validate script parameters # confirm script parameter $DockerHubRepository is <team name>/<project name> Confirm-DockerHubRepositoryFormatCorrect # confirm all the user-supplied paths exist Confirm-SourcePathsValid # for project $DockerHubRepository, get Docker image names and other details from online Docker hub # project tags data (format of data is PSObjects) [object[]]$HubImageDataPSObject = Get-DockerHubProjectImageInfo # now convert data in $HubImageDataPSObject to a hash table of hash tables for easier lookup/usage # *plus* add an entry for ContainerName - a safe/sanitized name to re/use for the container [hashtable]$HubImageDataHashTable = Convert-ImageDataToHashTables -ImageDataPSObjects $HubImageDataPSObject #region If user didn't specify any values for TestImageNames, display valid values and exit if ($TestImageNames.Count -eq 0) { Write-Output 'No image/tag name specified for TestImageTagName; please use a value below:' $HubImageDataHashTable.Keys | Sort-Object | ForEach-Object { Write-Output " $_" } exit } #endregion #region Validate script param TestImageNames # listing of valid, locally installed image names [string[]]$ValidTestImageTagNames = $null # check user supplied images names, if valid will be stored in ValidTestImageTagNames Confirm-ValidateUserImages -DockerHubRepositoryImageData $HubImageDataHashTable -ValidImageNames ([ref]$ValidTestImageTagNames) # check if no valid image names - exit if ($null -eq $ValidTestImageTagNames) { Write-Output 'No locally installed images to test against; exiting.' exit } #endregion #endregion #region Loop through valid local images, create/start container, copy code to it, run test and stop container if ($Quiet -eq $false) { Write-Output "Testing on these containers: $ValidTestImageTagNames" } # did all test scripts run successfully? start with $true but Invoke-TestScriptInDockerContainer # will set to $false if error [bool]$TestScriptSuccess = $true $ValidTestImageTagNames | ForEach-Object { $ValidTestImageTagName = $_ if ($Quiet -eq $false) { Write-Output ' '; Write-Output $ValidTestImageTagName } # get sanitized container name (based on repository + image name) for this image $ContainerName = ($HubImageDataHashTable[$ValidTestImageTagName]).ContainerName # get container info for $ContainerName $ContainerInfo = $null $AllContainerStatusInfo = Get-DockerContainerStatus if ($AllContainerStatusInfo -ne $null) { $ContainerInfo = $AllContainerStatusInfo | Where-Object { $_.Names -eq $ContainerName } } # if no container exists, create one and start it if ($ContainerInfo -eq $null) { # create Docker container and start it Initialize-DockerContainerAndStart -ImageName $ValidTestImageTagName -ContainerName $ContainerName } else { if ($Quiet -eq $false) { Write-Output ' Preexisting container found' } # if container not started, start it if ($ContainerInfo.Status.StartsWith('Up')) { if ($Quiet -eq $false) { Write-Output ' Container already started' } } else { # start local container Start-DockerContainer -ContainerName $ContainerName } } # temp folder path inside container [string]$ContainerTestFolderPath = $null Get-DockerContainerTempFolderPath -ContainerName $ContainerName ([ref]$ContainerTestFolderPath) # copy items in script param $SourcePaths to container $ContainerName to location # under folder $ContainerTestFolderPath; if error occurred, return $true # value in $ErrorOccurred but do not exit in Copy-FilesToDockerContainer [bool]$ErrorOccurred = $false Copy-FilesToDockerContainer -ContainerName $ContainerName -ContainerPath $ContainerTestFolderPath -ErrorOccurred ([ref]$ErrorOccurred) # only run test if no error occurred during copying if ($ErrorOccurred -eq $false) { # run test script in container $ContainerName at path $ContainerTestFolderPath/$TestFileAndParams # if error, do not exit so container can be stopped next step $ContainerScriptPath = Join-Path -Path $ContainerTestFolderPath -ChildPath $TestFileAndParams Invoke-TestScriptInDockerContainer -ContainerName $ContainerName -ScriptPath $ContainerScriptPath -TestScriptSuccess ([ref]$TestScriptSuccess) } # stop local container Stop-DockerContainer -ContainerName $ContainerName # if error occurred running test in container, exit now (i.e. after the container has been stopped) if ($TestScriptSuccess -eq $false) { # return TestScriptSuccess, which is false $TestScriptSuccess exit } } #endregion #region If -Quiet and no errors occurred, return $true # if not -Quiet, don't return any value BUT if -Quiet and everything # successful, return $true (best for automation) if ($Quiet -eq $true -and $TestScriptSuccess -eq $true) { $TestScriptSuccess } #endregion |