Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run Builds in Docker Container Locally #328

Closed
wants to merge 13 commits into from
Closed
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Documentation - VSTeam](https://img.shields.io/badge/Documentation-VSTeam-blue.svg)](https://github.com/DarqueWarrior/vsteam/blob/master/docs/readme.md)
[![PowerShell Gallery - VSTeam](https://img.shields.io/badge/PowerShell%20Gallery-VSTeam-blue.svg)](https://www.powershellgallery.com/packages/VSTeam)
[![Minimum Supported PowerShell Version](https://img.shields.io/badge/PowerShell-5.0-blue.svg)](https://github.com/PowerShell/PowerShell)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)

## Introduction

Expand Down Expand Up @@ -95,6 +95,8 @@ Builds the module, installs needed dependencies, loads the module into the sessi
.\Build-Module.ps1 -installDep -ipmo -buildHelp
```

#### Run Tests Locally

Runs all unit tests and executes the static code analysis.
```PowerShell
.\Build-Module.ps1 -runTests -codeCoverage -analyzeScript
Expand All @@ -105,7 +107,9 @@ Runs the tests, but executes only the unit tests that have the description "work
.\Build-Module.ps1 -runTests -testName workitems
```

#### Run Tests in Docker

You can [run your unit tests also locally](/tools/docker/RunTestsLocally.md) and cross-platform in a standardized environment.

## Contributors

Expand Down
278 changes: 278 additions & 0 deletions tools/docker/Run-ContainerTests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
[CmdletBinding()]
param (
#if you want to make sure that only linux based containers are used, since windows based containers are not supported on your OS
[Parameter(Mandatory=$false)]
[switch]
$UseLinux
)

function Set-DockerHost {
<#
.SYNOPSIS
Switch between windows and linux containers. IMPORTANT: Works only for windows based systems
#>
[CmdletBinding()]
param (
#sets the container OS
[Parameter(Mandatory = $true)]
[ValidateSet("windows", "linux")]
[string]
$Os
)


process {
$dockerVersion = docker version --format '{{json .}}' | ConvertFrom-Json

if ($dockerVersion.Server.Os -ne $Os ) {
& "$env:ProgramFiles\Docker\Docker\DockerCli.exe" -SwitchDaemon
}
else {
Write-Verbose "docker host is already set to $Os... ignoring command"
}
}
}

function Add-DockerBuild {
<#
.SYNOPSIS
Create a docker build with a tag. Optionally force to rebuild the container.
#>
[CmdletBinding()]
param (
# Path to the dockerfile
[Parameter(Mandatory = $true)]
[string]
$DockerFile,
# give it a named tag. Best with repository and image name
[Parameter(Mandatory = $true)]
[string]
$Tag,
# Force the rebuild of the container
[Switch]
$Force
)

process {
$dockerImageId = docker images -q $Tag

#only build if image does not exist or it's forced
#when not using force: It can't be checked if a docker file has is different from the existing image
if ($null -eq $dockerImageId -or $Force) {
docker build -f $DockerFile --tag $Tag .
}
else {
Write-Verbose "image $Tag already exists with id $dockerImageId"
}
}
}

function Start-DockerVSTeamTests {
[CmdletBinding()]
param (
# Name of the container to run
[Parameter(Mandatory = $true)]
[string]
$Container,
# volume mapping string
# see: https://docs.docker.com/storage/volumes/#start-a-container-with-a-volume
[Parameter(Mandatory = $true)]
[string]
$Volume,
# default directoy when the container is started
[Parameter(Mandatory = $true)]
[string]
$DefaultWorkDir,
# Image to start
[Parameter(Mandatory = $true)]
[string]
$Image,
# choose which shell to start
[Parameter(Mandatory = $false)]
[ValidateSet("pwsh", "powershell")]
[string]
$Shell = "pwsh",
# Use if powershell should wait for the container to exit
[Parameter(Mandatory = $false)]
[Switch]
$Wait,
# Open another powershell session to show the logs from the container
[Parameter(Mandatory = $false)]
[Switch]
$FollowLogs
)

begin {

# using a script block here to have syntax checking and highlightning.
# Later it is converted to a string to start the container with it
$pesterBuild = {

.\Build-Module.ps1 -installDep;
$null = Import-Module Pester;

$pesterArgs = [PesterConfiguration]::Default;
$pesterArgs.Run.Exit = $true;
$pesterArgs.Run.Path = '.\unit';
$pesterArgs.Run.PassThru = $true;
$pesterArgs.Output.Verbosity = 'Normal';
$pesterArgs.TestResult.Enabled = $true;
$pesterArgs.TestResult.OutputPath = '#Container#_result.xml'

Invoke-Pester -Configuration $pesterArgs;

# exist with PowerShells last exist code for docker.
# without deliverate exit code the -Wait switch could cause to wait indefinetely
Exit $LASTEXITCODE
}
}

process {

$containerId = docker ps --all --filter name=$Container -q
$containerIsRunning = $null -ne (docker ps --filter name=$Container -q)

if ($containerIsRunning) {
docker stop $containerId
}

if ($containerId) {
docker rm $Container
}

$psCommandString = ($pesterBuild.ToString()) -replace '#Container#', $Container

docker run `
-dit `
--name $Container `
--volume $Volume `
-w $DefaultWorkDir `
$Image `
$Shell -Command $psCommandString

if ($FollowLogs) {
$output = (docker exec -it $Container $Shell -c '$PSVersionTable | ConvertTo-Json -Compress;') -join ''
$outputFirst = $output.IndexOf('{')
$ouputLast = $output.LastIndexOf('}')
$versiontable = $output.Substring($outputFirst, $ouputLast+1-$outputFirst) | ConvertFrom-Json -Depth 50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConvertFrom-Json does not have a -Depth. Only ConvertTo-Json has that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it. Didnt get an error before and VSCode seems to suggest that as a parameter.

$psVersion = "$($versiontable.PSVersion.Major).$($versiontable.PSVersion.Minor).$($versiontable.PSVersion.Patch)"

$argList = "-NoExit -Command `"`$Host.UI.RawUI.WindowTitle = 'VSTeam Unit Tests | PowerShell $($versiontable.PSEdition) $psVersion | $($versiontable.Os)'; docker logs $Container -f`""
Start-Process $Shell -argumentlist $argList
}

if ($Wait) {
docker wait $Container
}

}
}

function Wait-DockerContainer {
<#
.SYNOPSIS
Wait for the given containers to finish. If they don't run, then error is thrown
#>
[CmdletBinding()]
param (
# Containers to wait for
[Parameter(Mandatory = $true)]
[string[]]
$Container
)

process {

$exitCodes = @()

$runningContainers = docker ps --format '{{json .}}' | ConvertFrom-Json

$notRunningContainers = @()
$notAllContainersRunning = $Container.Count -ne ($Container | Where-Object {

$contains = $runningContainers.Names.Contains($_)
if ($contains) {
return $true
}
else {
$notRunningContainers += $_
return $false
}

}).Count

if ($notAllContainersRunning) {
Write-Error "Contains with the following names are not running: $($notRunningContainers -join ', ')"
}
else {

$Container | ForEach-Object {
$exitCode = docker wait $_

$exitCodes += @{
containerName = $_
exitCode = $exitCode
}
}
}

return $exitCodes
}
}

$platform = $PSVersionTable.Platform
if ($platform -ne "Win32NT" -or $UseLinux) {
Write-Warning "Platform is not Win32NT based but '$platform'. Windows container do not work on linux based systems. Ignoring windows containers..."
}

$scriptPath = $PSScriptRoot
$rootDir = (Resolve-Path -Path "$scriptPath\..\..\").ToString().trim('\')
$containerFolder = "c:/vsteam"
$containerFilePath = "$rootDir/build/docker"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be "$rootDir/Tools/docker" not build/docker.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed the dir. I moved it before.


$dockerRepository = "vsteam"


$windowsImage = "$dockerRepository/wcore1903"
$windowsContainerPS7 = "$($dockerRepository)_wcore1903_ps7_tests"
$windowsContainerPS5 = "$($dockerRepository)_wcore1903_ps5_tests"

# Build / run windows based container
if ($platform -eq "Win32NT" -and !$UseLinux) {
Set-DockerHost -Os windows -Verbose
Add-DockerBuild -DockerFile "$containerFilePath/wcore1903/Dockerfile" -Tag $windowsImage

Start-DockerVSTeamTests `
-Container $windowsContainerPS7 `
-Volume "$rootDir`:$containerFolder" `
-DefaultWorkDir $containerFolder `
-Image $windowsImage `
-FollowLogs

Start-DockerVSTeamTests `
-Container $windowsContainerPS5 `
-Volume "$rootDir`:$containerFolder" `
-DefaultWorkDir $containerFolder `
-Image $windowsImage `
-Shell powershell `
-FollowLogs

$null = Wait-DockerContainer -Container @($windowsContainerPS5,$windowsContainerPS7)
}

$linuxImage = "$dockerRepository/linux"
$linuxContainer = "$($dockerRepository)_linux_ps7_tests"
$linuxContainerFolder = $containerFolder.Replace('c:/', '/c/')

# Build / run linux based container
if ($platform -eq "Win32NT" -and !$UseLinux) {
Set-DockerHost -Os linux -Verbose
}
Add-DockerBuild -DockerFile "$containerFilePath/linux/Dockerfile" -Tag $linuxImage
$null = Start-DockerVSTeamTests `
-Container $linuxContainer `
-Volume "$rootDir`:$linuxContainerFolder" `
-DefaultWorkDir $linuxContainerFolder `
-Image $linuxImage `
-Wait `
-FollowLogs
39 changes: 39 additions & 0 deletions tools/docker/RunTestsLocally.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Run Unit Tests Locally with Docker

When helping us to develop this module or when we want to expand the module. We always write unit tests. The problem with that is, that most of the time everybody is using his own machine with different version of OS or different versions of dependencies.

This is why we created docker files for windows and linux. Currently we have prepared to run the unit tests in

* linux - PowerShell Core 7
* windows server core - PowerShell 5
* windows server core - PowerShell Core 7

In best case you should only need to run the powershell script below. The dockerfiles being used for this have only the needed PowerShell modules installed. So theoretically you could also develop in theese containers with VSCode and remote development.

# Prerequisites

> **Note:** This feature is still in testing and was developed on a windows machine. Therefore, the script to run all tests for you works for that OS. But it is itended to work also for linux. But this comes a bit later.

* [Docker](https://docs.docker.com/engine/install/)
* depending on your OS you need to install Docker Desktop or only Server
* Windows based OS if you want to run Windows containers
* optional: [Install WSL](https://code.visualstudio.com/docs/remote/wsl#_installation)
* if you even [use WSL2](https://docs.microsoft.com/en-us/windows/wsl/install-win10#update-to-wsl-2) then container switch and run with much more performance!

Also be aware that we cannot know all prerequisites as there often many different dependencies for different host OS systems.

# How to Run

1. Install Docker: https://docs.docker.com/engine/install
2. Run [Run-ContainerTests.ps1](Run-ContainerTests.ps1) located under ./tools/docker

```powershell
#Example
Run-ContainerTests.ps1
```

# Limitations

* Windows container only work with windows based systems.
* If you want to use the container to develop with VSCode remote development, then it currently only works with linux systems.
* The log in PowerShell 5 window is scrambled, only the Pester results at the end can be properly observed.
23 changes: 23 additions & 0 deletions tools/docker/linux/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# escape=`
ARG fromTag=7.0.1-ubuntu-18.04
ARG pesterMinVer=5.0.0-rc8

FROM mcr.microsoft.com/powershell:${fromTag}

SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop';"]

RUN pwsh `
-NoLogo `
-NoProfile `
-Command " `
`$ProgressPreference = 'SilentlyContinue';`
Write-host 'Installing needed PowerShell modules...';`
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted;`
Install-Module -Name SHiPS -Repository PSGallery -SkipPublisherCheck;`
Install-Module -Name Trackyon.Utils -Repository PSGallery;`
Install-Module -Name Pester -Repository PSGallery -Force -AllowPrerelease -MinimumVersion '${pesterMinVer}' -AllowClobber -SkipPublisherCheck;`
Install-Module -Name PSSCriptAnalyzer -Repository PSGallery;`
Install-Module -Name Plaster -Repository PSGallery; `
"

CMD ["pwsh.exe"]
Loading