AutomatedLabTfs.psm1
#region Lab-specific functionality function Install-LabTeamFoundationEnvironment { [CmdletBinding()] param ( ) $tfsMachines = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object { -not $_.SkipDeployment -and -not (Test-LabTfsEnvironment -ComputerName $_.Name -NoDisplay).ServerDeploymentOk } $azDevOpsService = Get-LabVM -Role AzDevOps | Where-Object SkipDeployment foreach ($svcConnection in $azDevOpsService) { $role = $svcConnection.Roles | Where-Object Name -Match 'AzDevOps' # Override port or add if empty $role.Properties.Port = 443 $svcConnection.InternalNotes.Add('CertificateThumbprint', 'use SSL') if (-not $role.Properties.ContainsKey('PAT')) { Write-ScreenInfo -Type Error -Message "No Personal Access Token available for Azure DevOps connection to $svcConnection. You will be unable to deploy build workers and you will not be able to use the cmdlets New-LabReleasePipeline, Get-LabBuildStep, Get-LabReleaseStep. Consider adding the key PAT to your role properties hashtable." } if (-not $role.Properties.ContainsKey('Organisation')) { Write-ScreenInfo -Type Error -Message "No Organisation name available for Azure DevOps connection to $svcConnection. You will be unable to deploy build workers and you will not be able to use the cmdlets New-LabReleasePipeline, Get-LabBuildStep, Get-LabReleaseStep. Consider adding the key Organisation to your role properties hashtable where Organisation = dev.azure.com/<Organisation>" } } if ($azDevOpsService) { Export-Lab } $lab = Get-Lab $jobs = @() foreach ($machine in $tfsMachines) { Dismount-LabIsoImage -ComputerName $machine -SupressOutput $role = $machine.Roles | Where-Object Name -Match 'Tfs\d{4}|AzDevOps' $isoPath = ($lab.Sources.ISOs | Where-Object Name -eq $role.Name).Path $retryCount = 3 $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name] while (-not $autoLogon -and $retryCount -gt 0) { Enable-LabAutoLogon -ComputerName $machine Restart-LabVm -ComputerName $machine -Wait $autoLogon = (Test-LabAutoLogon -ComputerName $machine)[$machine.Name] $retryCount-- } if (-not $autoLogon) { throw "No logon session available for $($machine.InstallationUser.UserName). Cannot continue with TFS setup for $machine" } Mount-LabIsoImage -ComputerName $machine -IsoPath $isoPath -SupressOutput $jobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock { $startTime = (Get-Date) while (-not $dvdDrive -and (($startTime).AddSeconds(120) -gt (Get-Date))) { Start-Sleep -Seconds 2 if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { $dvdDrive = (Get-CimInstance -Class Win32_CDRomDrive | Where-Object MediaLoaded).Drive } else { $dvdDrive = (Get-WmiObject -Class Win32_CDRomDrive | Where-Object MediaLoaded).Drive } } if ($dvdDrive) { $executable = (Get-ChildItem -Path $dvdDrive -Filter *.exe).FullName $installation = Start-Process -FilePath $executable -ArgumentList '/quiet' -Wait -LoadUserProfile -PassThru if ($installation.ExitCode -notin 0, 3010) { throw "TFS Setup failed with exit code $($installation.ExitCode)" } Write-Verbose 'TFS Installation finished. Configuring...' } else { Write-Error -Message 'No ISO mounted. Cannot continue.' } } -AsJob -PassThru -NoDisplay } # If not already set, ignore certificate issues throughout the TFS interactions try { [ServerCertificateValidationCallback]::Ignore() } catch { } if ($tfsMachines) { Wait-LWLabJob -Job $jobs Restart-LabVm -ComputerName $tfsMachines -Wait Install-LabTeamFoundationServer } Install-LabBuildWorker Set-LabBuildWorkerCapability } function Install-LabTeamFoundationServer { [CmdletBinding()] param ( ) $tfsMachines = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object SkipDeployment -eq $false | Sort-Object { ($_.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps').Name } -Descending if (-not $tfsMachines) { return } # Assign unassigned build workers to our most current TFS machine Get-LabVM -Role TfsBuildWorker | Where-Object { -not ($_.Roles | Where-Object Name -eq TfsBuildWorker).Properties.ContainsKey('TfsServer') } | ForEach-Object { ($_.Roles | Where-Object Name -eq TfsBuildWorker).Properties.Add('TfsServer', $tfsMachines[0].Name) } $jobs = Install-LabWindowsFeature -ComputerName $tfsMachines -FeatureName Web-Mgmt-Tools -AsJob Write-ScreenInfo -Message 'Waiting for installation of IIS web admin tools to complete' -NoNewline Wait-LWLabJob -Job $jobs -ProgressIndicator 10 -Timeout $InstallationTimeout -NoDisplay $installationJobs = @() $count = 0 foreach ($machine in $tfsMachines) { if (Get-LabIssuingCA) { Write-ScreenInfo -Type Verbose -Message "Found CA in lab, requesting certificate" $cert = Request-LabCertificate -Subject "CN=$machine" -TemplateName WebServer -SAN $machine.AzureConnectionInfo.DnsName, $machine.FQDN, $machine.Name -ComputerName $machine -PassThru -ErrorAction Stop $machine.InternalNotes.Add('CertificateThumbprint', $cert.Thumbprint) Export-Lab } $role = $machine.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps' [string]$sqlServer = switch -Regex ($role.Name) { 'Tfs2015' { Get-LabVM -Role SQLServer2014 | Select-Object -First 1 } 'Tfs2017' { Get-LabVM -Role SQLServer2014, SQLServer2016 | Select-Object -First 1 } 'Tfs2018|AzDevOps' { Get-LabVM -Role SQLServer2017, SQLServer2019 | Select-Object -First 1 } default { throw 'No fitting SQL Server found in lab!' } } if (-not $sqlServer) { Write-Error 'No fitting SQL Server found in lab for TFS / Azure DevOps role.' -ErrorAction Stop } $initialCollection = 'AutomatedLab' $tfsPort = 8080 $databaseLabel = "TFS$count" # Increment database label in case we deploy multiple TFS [string]$machineName = $machine $count++ if ($role.Properties.ContainsKey('InitialCollection')) { $initialCollection = $role.Properties['InitialCollection'] } if ($role.Properties.ContainsKey('Port')) { $tfsPort = $role.Properties['Port'] } if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { if (-not (Get-LabAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $machine)) { (Get-Lab).AzureSettings.LoadBalancerPortCounter++ $remotePort = (Get-Lab).AzureSettings.LoadBalancerPortCounter Add-LWAzureLoadBalancedPort -ComputerName $machine -DestinationPort $tfsPort -Port $remotePort } if ($role.Properties.ContainsKey('Port')) { $machine.Roles.Where( { $_.Name -match 'Tfs\d{4}|AzDevOps' }).ForEach( { $_.Properties['Port'] = $tfsPort }) } else { $machine.Roles.Where( { $_.Name -match 'Tfs\d{4}|AzDevOps' }).ForEach( { $_.Properties.Add('Port', $tfsPort) }) } Export-Lab # Export lab again since we changed role properties } if ($role.Properties.ContainsKey('DbServer')) { [string]$sqlServer = Get-LabVM -ComputerName $role.Properties['DbServer'] -ErrorAction SilentlyContinue if (-not $sqlServer) { Write-ScreenInfo -Message "No SQL server called $($role.Properties['DbServer']) found in lab." -NoNewLine -Type Warning [string]$sqlServer = Get-LabVM -Role SQLServer2016, SQLServer2017, SQLServer2019 | Select-Object -First 1 Write-ScreenInfo -Message " Selecting $sqlServer instead." -Type Warning } } if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { # For good luck, disable the firewall again - in case Invoke-AzVmRunCommand failed to do its job. Invoke-LabCommand -ComputerName $machine, $sqlServer -NoDisplay -ScriptBlock { Set-NetFirewallProfile -All -Enabled False -PolicyStore PersistentStore } } Restart-LabVM -ComputerName $machine -Wait -NoDisplay $installationJobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock { $tfsConfigPath = (Get-ChildItem -Path $env:ProgramFiles -Filter tfsconfig.exe -Recurse | Select-Object -First 1).FullName if (-not $tfsConfigPath) { throw 'tfsconfig.exe could not be found.' } if (-not (Test-Path C:\DeployDebug)) { [void] (New-Item -Path C:\DeployDebug -ItemType Directory) } # Create unattend file with fitting parameters and replace all we can find [void] (Start-Process -FilePath $tfsConfigPath -ArgumentList 'unattend /create /type:Standard /unattendfile:C:\DeployDebug\TfsConfig.ini' -NoNewWindow -Wait) $config = (Get-Item -Path C:\DeployDebug\TfsConfig.ini -ErrorAction Stop).FullName $content = [System.IO.File]::ReadAllText($config) $content = $content -replace 'SqlInstance=.+', ('SqlInstance={0}' -f $sqlServer) $content = $content -replace 'DatabaseLabel=.+', ('DatabaseLabel={0}' -f $databaseLabel) $content = $content -replace 'UrlHostNameAlias=.+', ('UrlHostNameAlias={0}' -f $machineName) if ($cert.Thumbprint) { $content = $content -replace 'SiteBindings=.+', ('SiteBindings=https:*:{0}::My:{1}' -f $tfsPort, $cert.Thumbprint) $content = $content -replace 'PublicUrl=.+', ('PublicUrl=https://{0}:{1}' -f $machineName, $tfsPort) } else { $content = $content -replace 'SiteBindings=.+', ('SiteBindings=http:*:{0}:' -f $tfsPort) $content = $content -replace 'PublicUrl=.+', ('PublicUrl=http://{0}:{1}' -f $machineName, $tfsPort) } if ($cert.ThumbPrint -and $tfsConfigPath -match '14\.0') { Get-WebBinding -Name 'Team Foundation Server' | Remove-WebBinding New-WebBinding -Protocol https -Port $tfsPort -IPAddress * -Name 'Team Foundation Server' $binding = Get-Website -Name 'Team Foundation Server' | Get-WebBinding $binding.AddSslCertificate($cert.Thumbprint, "my") } $content = $content -replace 'webSiteVDirName=.+', 'webSiteVDirName=' $content = $content -replace 'CollectionName=.+', ('CollectionName={0}' -f $initialCollection) $content = $content -replace 'CollectionDescription=.+', 'CollectionDescription=Built by AutomatedLab, your friendly lab automation solution' $content = $content -replace 'WebSitePort=.+', ('WebSitePort={0}' -f $tfsPort) # Plain TFS 2015 $content = $content -replace 'UrlHostNameAlias=.+', ('UrlHostNameAlias={0}' -f $machineName) # Plain TFS 2015 [System.IO.File]::WriteAllText($config, $content) $command = "unattend /unattendfile:`"$config`" /continue" "`"$tfsConfigPath`" $command" | Set-Content C:\DeployDebug\SetupTfsServer.cmd $configurationProcess = Start-Process -FilePath $tfsConfigPath -ArgumentList $command -PassThru -NoNewWindow -Wait # Locate log files and cat them $log = Get-ChildItem -Path "$env:LOCALAPPDATA\Temp" -Filter dd_*_server_??????????????.log | Sort-Object -Property CreationTime | Select-Object -Last 1 $log | Get-Content if ($configurationProcess.ExitCode -ne 0) { throw ('Something went wrong while applying the unattended configuration {0}. Try {1} {2} manually. Read the log at {3}.' -f $config, $tfsConfigPath, $command, $log.FullName ) } } -Variable (Get-Variable sqlServer, machineName, InitialCollection, tfsPort, databaseLabel, cert -ErrorAction SilentlyContinue) -AsJob -ActivityName "TFS_Setup_$machine" -PassThru -NoDisplay } Write-ScreenInfo -Type Verbose -Message "Waiting for the installation of TFS on $tfsMachines to finish." Wait-LWLabJob -Job $installationJobs foreach ($job in $installationJobs) { $name = $job.Name.Replace('TFS_Setup_','') $type = if ($job.State -eq 'Completed') { 'Verbose' } else { 'Error' } $resultVariable = New-Variable -Name ("AL_TFSServer_$($name)_$([guid]::NewGuid().Guid)") -Scope Global -PassThru Write-ScreenInfo -Type $type -Message "TFS Deployment $($job.State.ToLower()) on '$($name)'. The job output of $job can be retrieved with `${$($resultVariable.Name)}" $resultVariable.Value = $job | Receive-Job -AutoRemoveJob -Wait } } function Install-LabBuildWorker { [CmdletBinding()] param ( ) $buildWorkers = Get-LabVM -Role TfsBuildWorker if (-not $buildWorkers) { return } $buildWorkerUri = Get-LabConfigurationItem -Name BuildAgentUri $buildWorkerPath = Join-Path -Path $labsources -ChildPath Tools\TfsBuildWorker.zip $download = Get-LabInternetFile -Uri $buildWorkerUri -Path $buildWorkerPath -PassThru Copy-LabFileItem -ComputerName $buildWorkers -Path $download.Path $installationJobs = @() foreach ($machine in $buildWorkers) { $role = $machine.Roles | Where-Object Name -eq TfsBuildWorker [int]$numberOfBuildWorkers = $role.Properties.NumberOfBuildWorkers $isOnDomainController = $machine -in (Get-LabVM -Role ADDS) $cred = $machine.GetLocalCredential() $tfsServer = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 $tfsPort = 8080 $skipServerDuringTest = $false # We want to skip testing public Azure DevOps endpoints if ($role.Properties.ContainsKey('Organisation') -and $role.Properties.ContainsKey('PAT')) { Write-ScreenInfo -Message "Deploying agent to Azure DevOps agent pool" -NoNewLine $tfsServer = 'dev.azure.com' $useSsl = $true $tfsPort = 443 $skipServerDuringTest = $true } elseif ($role.Properties.ContainsKey('TfsServer')) { $tfsServer = Get-LabVM -ComputerName $role.Properties['TfsServer'] -ErrorAction SilentlyContinue if (-not $tfsServer) { Write-ScreenInfo -Message "No TFS server called $($role.Properties['TfsServer']) found in lab." -NoNewLine -Type Warning $tfsServer = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 $role.Properties['TfsServer'] = $tfsServer.Name $shouldExport = $true Write-ScreenInfo -Message " Selecting $tfsServer instead." -Type Warning } $useSsl = $tfsServer.InternalNotes.ContainsKey('CertificateThumbprint') -or ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment) if ($useSsl) { $machine.InternalNotes.Add('CertificateThumpbrint', 'Use Ssl') $shouldExport = $true } } else { $useSsl = $tfsServer.InternalNotes.ContainsKey('CertificateThumbprint') -or ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment) if ($useSsl) { $machine.InternalNotes.Add('CertificateThumpbrint', 'Use Ssl') } $role.Properties.Add('TfsServer', $tfsServer.Name) $shouldExport = $true } if ($shouldExport) { Export-Lab } $tfsTest = Test-LabTfsEnvironment -ComputerName $tfsServer -NoDisplay -SkipServer:$skipServerDuringTest if ($tfsTest.ServerDeploymentOk -and $tfsTest.BuildWorker[$machine.Name].WorkerDeploymentOk) { Write-ScreenInfo -Message "Build worker $machine assigned to $tfsServer appears to be configured. Skipping..." continue } $tfsRole = $tfsServer.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps' if ($tfsRole -and $tfsRole.Properties.ContainsKey('Port')) { $tfsPort = $tfsRole.Properties['Port'] } [string]$machineName = $tfsServer if ((Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and -not ($tfsServer.Roles.Name -eq 'AzDevOps' -and $tfsServer.SkipDeployment)) { $tfsPort = (Get-LabAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $tfsServer -ErrorAction SilentlyContinue).Port $machineName = $tfsServer.AzureConnectionInfo.DnsName if (-not $tfsPort) { Write-Error -Message 'There has been an error setting the Azure port during TFS installation. Cannot continue installing build worker.' return } } $pat = if ($role.Properties.ContainsKey('PAT')) { $role.Properties['PAT'] $machineName = "dev.azure.com/$($role.Properties['Organisation'])" } elseif ($tfsRole.Properties.ContainsKey('PAT')) { $tfsRole.Properties['PAT'] $machineName = "dev.azure.com/$($tfsRole.Properties['Organisation'])" } else { [string]::Empty } $agentPool = if ($role.Properties.ContainsKey('AgentPool')) { $role.Properties['AgentPool'] } else { 'default' } $installationJobs += Invoke-LabCommand -ComputerName $machine -ScriptBlock { if (-not (Test-Path C:\TfsBuildWorker.zip)) { throw 'Build worker installation files not available' } if ($numberOfBuildWorkers) { $numberOfBuildWorkers = 1..$numberOfBuildWorkers } else { $numberOfBuildWorkers = 1 } foreach ($numberOfBuildWorker in $numberOfBuildWorkers) { Microsoft.PowerShell.Archive\Expand-Archive -Path C:\TfsBuildWorker.zip -DestinationPath "C:\BuildWorker$numberOfBuildWorker" -Force $configurationTool = Get-Item "C:\BuildWorker$numberOfBuildWorker\config.cmd" -ErrorAction Stop $content = if ($useSsl -and [string]::IsNullOrEmpty($pat)) { "$configurationTool --unattended --url https://$($machineName):$($tfsPort) --auth Integrated --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --sslskipcertvalidation --gituseschannel" } elseif ($useSsl -and -not [string]::IsNullOrEmpty($pat)) { "$configurationTool --unattended --url https://$($machineName) --auth pat --token $pat --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --sslskipcertvalidation --gituseschannel" } elseif (-not $useSsl -and -not [string]::IsNullOrEmpty($pat)) { "$configurationTool --unattended --url http://$($machineName) --auth pat --token $pat --pool $agentPool --agent $($env:COMPUTERNAME)-$numberOfBuildWorker --runasservice --gituseschannel" } else { "$configurationTool --unattended --url http://$($machineName):$($tfsPort) --auth Integrated --pool $agentPool --agent $env:COMPUTERNAME --runasservice --gituseschannel" } if ($isOnDomainController) { $content += " --windowsLogonAccount $($cred.UserName) --windowsLogonPassword $($cred.GetNetworkCredential().Password)" } $null = New-Item -ItemType Directory -Path C:\DeployDebug -ErrorAction SilentlyContinue Set-Content -Path "C:\DeployDebug\SetupBuildWorker$numberOfBuildWorker.cmd" -Value $content -Force $configResult = & "C:\DeployDebug\SetupBuildWorker$numberOfBuildWorker.cmd" $log = Get-ChildItem -Path "C:\BuildWorker$numberOfBuildWorker\_diag" -Filter *.log | Sort-Object -Property CreationTime | Select-Object -Last 1 [pscustomobject]@{ ConfigResult = $configResult LogContent = $log | Get-Content } if ($LASTEXITCODE -notin 0, 3010) { Write-Warning -Message "Build worker $numberOfBuildWorker on '$env:COMPUTERNAME' failed to install. Exit code was $($LASTEXITCODE). Log is $($Log.FullName)" } } } -AsJob -Variable (Get-Variable machineName, tfsPort, useSsl, pat, isOnDomainController, cred, numberOfBuildWorkers, agentPool) -ActivityName "TFS_Agent_$machine" -PassThru -NoDisplay } Wait-LWLabJob -Job $installationJobs foreach ($job in $installationJobs) { $name = $job.Name.Replace('TFS_Agent_','') $type = if ($job.State -eq 'Completed') { 'Verbose' } else { 'Error' } $resultVariable = New-Variable -Name ("AL_TFSAgent_$($name)_$([guid]::NewGuid().Guid)") -Scope Global -PassThru Write-ScreenInfo -Type $type -Message "TFS Agent deployment $($job.State.ToLower()) on '$($name)'. The job output of $job can be retrieved with `${$($resultVariable.Name)}" $resultVariable.Value = $job | Receive-Job -AutoRemoveJob -Wait } } function Set-LabBuildWorkerCapability { [CmdletBinding()] param ( ) $buildWorkers = Get-LabVM -Role TfsBuildWorker if (-not $buildWorkers) { return } foreach ($machine in $buildWorkers) { $role = $machine.Roles | Where-Object Name -eq TfsBuildWorker $agentPool = if ($role.Properties.ContainsKey('AgentPool')) { $role.Properties['AgentPool'] } else { 'default' } [int]$numberOfBuildWorkers = $role.Properties.NumberOfBuildWorkers if ((Get-Command -Name Add-TfsAgentUserCapability -ErrorAction SilentlyContinue) -and $role.Properties.ContainsKey('Capabilities')) { $bwParam = Get-LabTfsParameter -ComputerName $machine if ($numberOfBuildWorkers) { $range = 1..$numberOfBuildWorkers } else { $range = 1 } foreach ($numberOfBuildWorker in $range) { $agt = Get-TfsAgent @bwParam -PoolName $agentPool -Filter ([scriptblock]::Create("`$_.name -eq '$($machine.Name)-$numberOfBuildWorker'")) $caps = @{} foreach ($prop in ($role.Properties['Capabilities'] | ConvertFrom-Json).PSObject.Properties) { $caps[$prop.Name] = $prop.Value } $null = Add-TfsAgentUserCapability @bwParam -Capability $caps -Agent $agt -PoolName $agentPool } } } } #endregion #region TFS-specific functionality function New-LabReleasePipeline { [CmdletBinding(DefaultParameterSetName = 'CloneRepo')] param ( [string] $ProjectName = 'ALSampleProject', [Parameter(Mandatory, ParameterSetName = 'CloneRepo')] [Parameter(ParameterSetName = 'LocalSource')] [string] $SourceRepository, [Parameter(Mandatory, ParameterSetName = 'LocalSource')] [string] $SourcePath, [ValidateSet('Git', 'FileCopy')] [string]$CodeUploadMethod = 'Git', [string] $ComputerName, [hashtable[]] $BuildSteps, [hashtable[]] $ReleaseSteps ) if (-not (Get-Lab -ErrorAction SilentlyContinue)) { throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server' } if ($CodeUploadMethod -eq 'Git' -and -not $SourceRepository) { throw "Using the code upload method 'Git' requires a source repository to be defined." } $tfsVm = if ($ComputerName) { Get-LabVM -ComputerName $ComputerName } else { Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 } if (-not $tfsVm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) } $localLabSources = Get-LabSourcesLocationInternal -Local $role = $tfsVm.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps' $originalPort = 8080 $tfsInstance = $tfsVm.FQDN if ($role.Properties.ContainsKey('Port')) { $originalPort = $role.Properties['Port'] } $gitBinary = if (Get-Command git) { (Get-Command git).Source } elseif (Test-Path -Path $localLabSources\Tools\git.exe) { "$localLabSources\Tools\git.exe" } if (-not $gitBinary) { Write-ScreenInfo -Message 'Git is not installed. We are not be able to push any code to the remote repository and cannot proceed. Please install Git' return } $defaultParam = Get-LabTfsParameter -ComputerName $tfsVm $defaultParam.ProjectName = $ProjectName $project = New-TfsProject @defaultParam -SourceControlType Git -TemplateName 'Agile' -Timeout (New-TimeSpan -Minutes 5) $repository = Get-TfsGitRepository @defaultParam if ($CodeUploadMethod -eq 'git' -and -not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { $repository.remoteUrl = $repository.remoteUrl -replace $originalPort, $defaultParam.Port if ($repository.remoteUrl -match 'http(s?)://(?<Host>[\w\.]+):') { $repository.remoteUrl = $repository.remoteUrl.Replace($Matches.Host, $tfsVm.AzureConnectionInfo.DnsName) } } if ($CodeUploadMethod -eq 'FileCopy' -and -not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { if ($repository.remoteUrl -match 'http(s?)://(?<Host>[\w\.]+):') { $repository.remoteUrl = $repository.remoteUrl.Replace($Matches.Host, $tfsVm.FQDN) } } if ($SourceRepository) { if (-not $gitBinary) { Write-Error "Git.exe could not be located, cannot clone repository from '$SourceRepository'" return } # PAT URLs look like https://{yourPAT}@dev.azure.com/yourOrgName/yourProjectName/_git/yourRepoName $repoUrl = if ($defaultParam.Contains('PersonalAccessToken')) { $tmp = $repository.remoteUrl.Insert($repository.remoteUrl.IndexOf('/') + 2, '{0}@') $tmp -f $defaultParam.PersonalAccessToken } else { $tmp = $repository.remoteUrl.Insert($repository.remoteUrl.IndexOf('/') + 2, '{0}:{1}@') $tmp -f $defaultParam.Credential.GetNetworkCredential().UserName.ToLower(), $defaultParam.Credential.GetNetworkCredential().Password } Write-ScreenInfo -Type Verbose -Message "Generated repo url $repoUrl" if (-not $SourcePath) { $SourcePath = "$localLabSources\GitRepositories\$((Get-Lab).Name)" } if (-not (Test-Path -Path $SourcePath)) { Write-ScreenInfo -Type Verbose -Message "Creating $SourcePath to contain your cloned repos" [void] (New-Item -ItemType Directory -Path $SourcePath -Force) } $repositoryPath = Join-Path -Path $SourcePath -ChildPath (Split-Path -Path $SourceRepository -Leaf) if (-not (Test-Path $repositoryPath)) { Write-ScreenInfo -Type Verbose -Message "Creating $repositoryPath to contain your cloned repo" [void] (New-Item -ItemType Directory -Path $repositoryPath) } Push-Location Set-Location -Path $repositoryPath if (Join-Path -Path $repositoryPath -ChildPath '.git' -Resolve -ErrorAction SilentlyContinue) { Write-ScreenInfo -Type Verbose -Message ('There already is a clone of {0} in {1}. Pulling latest changes from remote if possible.' -f $SourceRepository, $repositoryPath) try { $errorFile = [System.IO.Path]::GetTempFileName() $pullResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'pull', 'origin') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile if ($pullResult.ExitCode -ne 0) { Write-ScreenInfo -Type Warning -Message "Could not pull from $SourceRepository. Git returned: $(Get-Content -Path $errorFile)" } } finally { Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue } } else { Write-ScreenInfo -Type Verbose -Message ('Cloning {0} in {1}.' -f $SourceRepository, $repositoryPath) try { $retries = 3 $errorFile = [System.IO.Path]::GetTempFileName() $cloneResult = Start-Process -FilePath $gitBinary -ArgumentList @('clone', $SourceRepository, $repositoryPath, '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile while ($cloneResult.ExitCode -ne 0 -and $retries -gt 0) { Write-ScreenInfo "Could not clone the repository '$SourceRepository', retrying ($retries)..." Start-Sleep -Seconds 5 $cloneResult = Start-Process -FilePath $gitBinary -ArgumentList @('clone', $SourceRepository, $repositoryPath, '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile $retries-- } if ($cloneResult.ExitCode -ne 0) { Write-Error "Could not clone from $SourceRepository. Git returned: $(Get-Content -Path $errorFile)" } } finally { Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue } } Pop-Location } if ($CodeUploadMethod -eq 'Git') { Push-Location Set-Location -Path $repositoryPath try { $errorFile = [System.IO.Path]::GetTempFileName() $addRemoteResult = Start-Process -FilePath $gitBinary -ArgumentList @('remote', 'add', 'tfs', $repoUrl) -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile if ($addRemoteResult.ExitCode -ne 0) { Write-Error "Could not add remote tfs to $repoUrl. Git returned: $(Get-Content -Path $errorFile)" } } finally { Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue } try { $pattern = '(?>remotes\/origin\/)(?<BranchName>[\w\/]+)' $branches = git branch -a | Where-Object { $_ -cnotlike '*HEAD*' -and $_ -like ' remotes/origin*' } foreach ($branch in $branches) { $branch -match $pattern | Out-Null $null = git checkout $Matches.BranchName 2>&1 if ($LASTEXITCODE -eq 0) { $retries = 3 $errorFile = [System.IO.Path]::GetTempFileName() $pushResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'push', 'tfs', '--all', '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile while ($pushResult.ExitCode -ne 0 -and $retries -gt 0) { Write-ScreenInfo "Could not push the repository in '$pwd' to TFS, retrying ($retries)..." Start-Sleep -Seconds 5 $pushResult = Start-Process -FilePath $gitBinary -ArgumentList @('-c', 'http.sslVerify=false', 'push', 'tfs', '--all', '--quiet') -Wait -NoNewWindow -PassThru -RedirectStandardError $errorFile $retries-- } if ($pushResult.ExitCode -ne 0) { Write-Error "Could not push to $repoUrl. Git returned: $(Get-Content -Path $errorFile)" } } } } finally { Remove-Item -Path $errorFile -Force -ErrorAction SilentlyContinue } Pop-Location Write-ScreenInfo -Type Verbose -Message ('Pushed code from {0} to remote {1}' -f $SourceRepository, $repoUrl) } else { $remoteGitBinary = Invoke-LabCommand -ActivityName 'Test Git availibility' -ComputerName $tfsVm -ScriptBlock { if (Get-Command git) { (Get-Command git).Source } elseif (Test-Path -Path $localLabSources\Tools\git.exe) { "$localLabSources\Tools\git.exe" } } -PassThru if (-not $remoteGitBinary) { Write-ScreenInfo -Message "Git is not installed on '$tfsVm'. We are not be able to push any code to the remote repository and cannot proceed. Please install Git on '$tfsVm'" return } $repoDestination = if ($IsLinux -or $IsMacOs) { "/$ProjectName.temp" } else { "C:\$ProjectName.temp" } if ($repositoryPath) { Copy-LabFileItem -Path $repositoryPath -ComputerName $tfsVm -DestinationFolderPath $repoDestination -Recurse } else { Copy-LabFileItem -Path $SourcePath -ComputerName $tfsVm -DestinationFolderPath $repoDestination -Recurse } Invoke-LabCommand -ActivityName 'Push code to TFS/AZDevOps' -ComputerName $tfsVm -ScriptBlock { Set-Location -Path "C:\$ProjectName.temp\$ProjectName" git remote add tfs $repoUrl $pattern = '(?>remotes\/origin\/)(?<BranchName>[\w\/]+)' $branches = git branch -a | Where-Object { $_ -cnotlike '*HEAD*' -and -not $_.StartsWith('*') } foreach ($branch in $branches) { if ($branch -match $pattern) { $null = git checkout $Matches.BranchName 2>&1 if ($LASTEXITCODE -eq 0) { git add . 2>&1 git commit -m 'Initial' 2>&1 git -c http.sslVerify=false push --set-upstream tfs $Matches.BranchName 2>&1 } } } Set-Location -Path C:\ Remove-Item -Path "C:\$ProjectName.temp" -Recurse -Force } -Variable (Get-Variable -Name repoUrl, ProjectName) } if (-not ($role.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment)) { Invoke-LabCommand -ActivityName 'Clone local repo from TFS' -ComputerName $tfsVm -ScriptBlock { if (-not (Test-Path -Path C:\Git)) { New-Item -ItemType Directory -Path C:\Git | Out-Null } Set-Location -Path C:\Git git -c http.sslVerify=false clone $repoUrl 2>&1 } -Variable (Get-Variable -Name repoUrl, ProjectName) } if ($BuildSteps.Count -gt 0) { $buildParameters = $defaultParam.Clone() $buildParameters.DefinitionName = "$($ProjectName)Build" $buildParameters.BuildTasks = $BuildSteps New-TfsBuildDefinition @buildParameters } if ($ReleaseSteps.Count -gt 0) { $releaseParameters = $defaultParam.Clone() $releaseParameters.ReleaseName = "$($ProjectName)Release" $releaseParameters.ReleaseTasks = $ReleaseSteps New-TfsReleaseDefinition @releaseParameters } } function Get-LabBuildStep { param ( [string] $ComputerName ) if (-not (Get-Lab -ErrorAction SilentlyContinue)) { throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server' } $tfsvm = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 if ($ComputerName) { $tfsVm = Get-LabVm -ComputerName $ComputerName } if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) } $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm $defaultParam.ProjectName = $ProjectName return (Get-TfsBuildStep @defaultParam) } function Get-LabReleaseStep { param ( [string] $ComputerName ) if (-not (Get-Lab -ErrorAction SilentlyContinue)) { throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server' } $tfsvm = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 if ($ComputerName) { $tfsVm = Get-LabVm -ComputerName $ComputerName } if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) } $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm $defaultParam.ProjectName = $ProjectName return (Get-TfsReleaseStep @defaultParam) } function Get-LabTfsUri { [CmdletBinding()] param ( [string] $ComputerName ) if (-not (Get-Lab -ErrorAction SilentlyContinue)) { throw 'No lab imported. Please use Import-Lab to import the target lab containing at least one TFS server' } $tfsvm = Get-LabVM -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Select-Object -First 1 if ($ComputerName) { $tfsVm = Get-LabVM -ComputerName $ComputerName } if (-not $tfsvm) { throw ('No TFS VM in lab or no machine found with name {0}' -f $ComputerName) } $defaultParam = Get-LabTfsParameter -ComputerName $tfsvm if (($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or $defaultParam.Contains('PersonalAccessToken')) { 'https://{0}/{1}' -f $defaultParam.InstanceName, $defaultParam.CollectionName } elseif ($defaultParam.UseSsl) { 'https://{0}:{1}@{2}:{3}/{4}' -f $defaultParam.Credential.GetNetworkCredential().UserName, $defaultParam.Credential.GetNetworkCredential().Password, $defaultParam.InstanceName, $defaultParam.Port, $defaultParam.CollectionName } else { 'http://{0}:{1}@{2}:{3}/{4}' -f $defaultParam.Credential.GetNetworkCredential().UserName, $defaultParam.Credential.GetNetworkCredential().Password, $defaultParam.InstanceName, $defaultParam.Port, $defaultParam.CollectionName } } function Test-LabTfsEnvironment { param ( [Parameter(Mandatory)] [string] $ComputerName, [switch] $SkipServer, [switch] $SkipWorker, [switch] $NoDisplay ) $lab = Get-Lab -ErrorAction Stop $machine = Get-LabVm -Role Tfs2015, Tfs2017, Tfs2018, AzDevOps | Where-Object -Property Name -eq $ComputerName $assignedBuildWorkers = Get-LabVm -Role TfsBuildWorker | Where-Object { ($_.Roles | Where-Object Name -eq TfsBuildWorker)[0].Properties['TfsServer'] -eq $machine.Name -or ` ($_.Roles | Where-Object Name -eq TfsBuildWorker)[0].Properties.ContainsKey('PAT') } if (-not $machine -and -not $SkipServer.IsPresent) { return } if (-not $script:tfsDeploymentStatus) { $script:tfsDeploymentStatus = @{ } } if (-not $script:tfsDeploymentStatus.ContainsKey($ComputerName)) { $script:tfsDeploymentStatus[$ComputerName] = @{ServerDeploymentOk = $SkipServer.IsPresent; BuildWorker = @{ } } } if (-not $script:tfsDeploymentStatus[$ComputerName].ServerDeploymentOk) { $uri = Get-LabTfsUri -ComputerName $machine -ErrorAction SilentlyContinue if ($null -eq $uri) { Write-PSFMessage -Message "TFS URI could not be determined." return $script:tfsDeploymentStatus[$ComputerName] } $defaultParam = Get-LabTfsParameter -ComputerName $machine $defaultParam.ErrorAction = 'Stop' $defaultParam.ErrorVariable = 'apiErr' try { $param = @{ Method = 'Get' Uri = $uri ErrorAction = 'Stop' } if ($PSEdition -eq 'Core' -and (Get-Command Invoke-RestMethod).Parameters.ContainsKey('SkipCertificateCheck')) { $param.SkipCertificateCheck = $true } if ($accessToken) { $param.Headers = @{Authorization = Get-TfsAccessTokenString -PersonalAccessToken $accessToken } } else { $param.Credential = $defaultParam.credential } $null = Invoke-RestMethod @param } catch { Write-ScreenInfo -Type Error -Message "TFS URI $uri could not be accessed. Exception: $($_.Exception)" return $script:tfsDeploymentStatus[$ComputerName] } try { $null = Get-TfsProject @defaultParam } catch { Write-ScreenInfo -Type Error -Message "TFS URI $uri accessible, but no API call was possible. Exception: $($apiErr)" return $script:tfsDeploymentStatus[$ComputerName] } $script:tfsDeploymentStatus[$ComputerName].ServerDeploymentOk = $true } foreach ($worker in $assignedBuildWorkers) { if ($script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name].WorkerDeploymentOk) { continue } if (-not $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name]) { $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name] = @{WorkerDeploymentOk = $SkipWorker.IsPresent } } if ($SkipWorker.IsPresent) { continue } $svcRunning = Invoke-LabCommand -PassThru -ComputerName $worker -ScriptBlock { Get-Service -Name *vsts* } -NoDisplay $script:tfsDeploymentStatus[$ComputerName].BuildWorker[$worker.Name].WorkerDeploymentOk = $svcRunning.Status -eq 'Running' } return $script:tfsDeploymentStatus[$ComputerName] } function Open-LabTfsSite { param ( [string] $ComputerName ) Start-Process -FilePath (Get-LabTfsUri @PSBoundParameters) } function New-LabTfsFeed { param ( [Parameter(Mandatory)] [string] $ComputerName, [Parameter(Mandatory)] [string] $FeedName, [object[]] $FeedPermissions, [switch] $PassThru ) $tfsVm = Get-LabVM -ComputerName $computerName $role = $tfsVm.Roles | Where-Object Name -match 'Tfs\d{4}|AzDevOps' $defaultParam = Get-LabTfsParameter -ComputerName $ComputerName $defaultParam['FeedName'] = $FeedName $defaultParam['ApiVersion'] = '5.0-preview.1' try { New-TfsFeed @defaultParam -ErrorAction Stop if ($FeedPermissions) { Set-TfsFeedPermission @defaultParam -Permissions $FeedPermissions } } catch { Write-Error $_ } if ($PassThru) { Get-LabTfsFeed -ComputerName $ComputerName -FeedName $FeedName } } function Get-LabTfsFeed { param ( [Parameter(Mandatory)] [string] $ComputerName, [string] $FeedName ) $lab = Get-Lab $tfsVm = Get-LabVM -ComputerName $ComputerName $defaultParam = Get-LabTfsParameter -ComputerName $ComputerName $defaultParam['FeedName'] = $FeedName $defaultParam['ApiVersion'] = '5.0-preview.1' $feed = Get-TfsFeed @defaultParam if (-not $tfsVm.SkipDeployment -and $(Get-Lab).DefaultVirtualizationEngine -eq 'Azure') { if ($feed.url -match 'http(s?)://(?<Host>[\w\.]+):(?<Port>\d+)/') { $feed.url = $feed.url.Replace($Matches.Host, $tfsVm.AzureConnectionInfo.DnsName) $feed.url = $feed.url.Replace($Matches.Port, $defaultParam.Port) } } if ($feed.url -match '(?<url>http.*)\/_apis') { $nugetV2Url = '{0}/_packaging/{1}/nuget/v2' -f $Matches.url, $feed.name $feed | Add-Member -Name NugetV2Url -MemberType NoteProperty $nugetV2Url $feed | Add-Member -Name NugetCredential -MemberType NoteProperty ($tfsVm.GetCredential($lab)) $nugetApiKey = '{0}@{1}:{2}' -f $feed.NugetCredential.GetNetworkCredential().UserName, $feed.NugetCredential.GetNetworkCredential().Domain, $feed.NugetCredential.GetNetworkCredential().Password $feed | Add-Member -Name NugetApiKey -MemberType NoteProperty -Value $nugetApiKey } $feed } function Get-LabTfsParameter { param ( [Parameter(Mandatory)] [string] $ComputerName, [switch] $Local ) $lab = Get-Lab $tfsVm = Get-LabVM -ComputerName $ComputerName $role = $tfsVm.Roles | Where-Object -Property Name -match 'Tfs\d{4}|AzDevOps' $bwRole = $tfsVm.Roles | Where-Object -Property Name -eq TfsBuildWorker $initialCollection = 'AutomatedLab' $tfsPort = 8080 $tfsInstance = if (-not $bwRole) {$tfsVm.FQDN} else {$bwRole.Properties.TfsServer} if ($role -and $role.Properties.ContainsKey('Port')) { $tfsPort = $role.Properties['Port'] } if ($bwRole -and $bwRole.Properties.ContainsKey('Port')) { $tfsPort = $bwRole.Properties['Port'] } if (-not $Local.IsPresent -and (Get-Lab).DefaultVirtualizationEngine -eq 'Azure' -and -not ($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment)) { $tfsPort = if ($bwRole) { (Get-LWAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $bwRole.Properties.TfsServer -ErrorAction SilentlyContinue).FrontendPort } else { (Get-LWAzureLoadBalancedPort -DestinationPort $tfsPort -ComputerName $tfsVm -ErrorAction SilentlyContinue).FrontendPort } if (-not $tfsPort) { Write-Error -Message 'There has been an error setting the Azure port during TFS installation. Cannot continue rolling out release pipeline' return } $tfsInstance = if ($bwRole) { (Get-LabVm $bwRole.Properties.TfsServer).AzureConnectionInfo.DnsName } else { $tfsVm.AzureConnectionInfo.DnsName } } if ($role -and $role.Properties.ContainsKey('InitialCollection')) { $initialCollection = $role.Properties['InitialCollection'] } if ($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) { $tfsInstance = 'dev.azure.com' $initialCollection = $role.Properties['Organisation'] $accessToken = $role.Properties['PAT'] $tfsPort = 443 } if ($bwRole -and $bwRole.Properties.ContainsKey('Organisation')) { $tfsInstance = 'dev.azure.com' $initialCollection = $bwRole.Properties['Organisation'] $accessToken = $bwRole.Properties['PAT'] $tfsPort = 443 } if (-not $role) { $tfsVm = Get-LabVm -ComputerName $bwrole.Properties.TfsServer $role = $tfsVm.Roles | Where-Object -Property Name -match 'Tfs\d{4}|AzDevOps' } $credential = $tfsVm.GetCredential((Get-Lab)) $useSsl = $tfsVm.InternalNotes.ContainsKey('CertificateThumbprint') -or ($role.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or ($bwRole -and $bwRole.Properties.ContainsKey('Organisation')) $defaultParam = @{ InstanceName = $tfsInstance Port = $tfsPort CollectionName = $initialCollection UseSsl = $useSsl SkipCertificateCheck = $true } $defaultParam.ApiVersion = switch ($role.Name) { 'Tfs2015' { '2.0'; break } 'Tfs2017' { '3.0'; break } { $_ -match '2018|AzDevOps' } { '4.0'; break } default { '2.0' } } if (($tfsVm.Roles.Name -eq 'AzDevOps' -and $tfsVm.SkipDeployment) -or ($bwRole -and $bwRole.Properties.ContainsKey('Organisation'))) { $defaultParam.ApiVersion = '5.1' } if ($accessToken) { $defaultParam.PersonalAccessToken = $accessToken } elseif ($credential) { $defaultParam.Credential = $credential } else { Write-ScreenInfo -Type Error -Message 'Neither Credential nor AccessToken are available. Unable to continue' return } $defaultParam } #endregion |