tasks/container.tasks.ps1

# ContainersToBuild must be an array of hashtables of the following structure:
# @(
# @{
# Dockerfile = "<path-to-dockerfile>"
# ImageName = "<container-image-name-without-tag>"
# ContextDir = "<path-to-docker-build-context-dir>"
# Arguments = @{ Name = "Value" }
# }
# )
$ContainersToBuild = @()
$UseAcrTasks = $false
$SkipPublishContainerImages = $false

# Allows any containers to be built with a custom version tag, rather than the default SemVer provided by GitVersion
$ContainerImageVersionOverride = $null
$ContainerRegistryPublishPrefix = $null
$ContainerImageTagArtefactPath = Join-Path $PWD "image-tag"

# Allow cross-subscription ACR access
$AcrSubscription = ""

# Default settings for Docker Registry publishing
$DockerRegistryFqdn = "docker.io"
$DockerRegistryUsername = $null


# Synopsis: Build Container Images
task BuildContainerImages -If {!$SkipContainerImages -and $ContainersToBuild} GenerateContainerBuildTag,{

    # If building images locally, check whether a Docker daemon is available so we can fail fast with a useful error message
    try {
        if (!$UseAcrTasks) {
            exec { docker ps }
        }
    }
    catch {
        throw "Unable to build container images - Docker is not installed or not running"
    }

    foreach ($buildInfo in $ContainersToBuild) {
        $contextDir = $buildInfo.ContainsKey("ContextDir") ? $buildInfo.ContextDir : (Split-Path -Parent $buildInfo.Dockerfile)
        $buildTag = "{0}:{1}" -f $buildInfo.ImageName.ToLower(), $containerBuildTag

        $containerBuildArgs = $null
        if ($buildInfo.Arguments) {
            $containerBuildArgs = $buildInfo.Arguments.Keys | % {
                # Support deferred evaluation of container build arguments
                $argValue = $buildInfo.Arguments[$_] -is [scriptblock] ? $buildInfo.Arguments[$_].Invoke() : $buildInfo.Arguments[$_]

                # Construct the command-line arguments required by 'docker build' and 'az acr build'
                "--build-arg $_=$argValue"
            }            
        }

        # Setup common build parameters
        $buildParameters = @(
            "--file $($buildInfo.Dockerfile)"
        )
        if ($containerBuildArgs) {
            $buildParameters += $containerBuildArgs -join " "
        }

        if ($buildInfo.Target) {
            $buildParameters += "--target $($buildInfo.Target)"
        }

        if ($UseAcrTasks) {
            if (!(Test-AzCliConnection)) {
                throw "You must be logged in to Azure CLI to build using ACR Tasks"
            }

            Write-Build White "Building & publishing image with ACR Tasks"
            $acrPublishTag = $ContainerRegistryPublishPrefix ? `
                                "$ContainerRegistryPublishPrefix/$buildTag" : `
                                $buildTag

            # Set command-line for building via ACR Tasks
            # Since images are published automatically when built via ACR Tasks, we need to tag the image with a '--pre'
            # suffix to signify that we don't yet know whether this build will be good. (i.e.when running in CI the tests
            # will be running in parallel so they could fail after the image has been published)
            $buildParameters += @(
                "-t $acrPublishTag--pre"
                "--registry $ContainerRegistryFqdn"
            )

            # Support cross-subscription ACR access
            if ($AcrSubscription) {
                $buildParameters += "--subscription $AcrSubscription"
            }

            $buildCmd = "az acr build"
        }
        else {
            # Set command-line for building with docker
            $buildParameters += "-t $buildTag"
            $buildCmd = "docker build"               
        }

        # Add the final positional command-line argument
        $buildParameters += $contextDir

        # Form the command-line
        $buildCmdline = "$buildCmd $($buildParameters -join " ")"
        Write-Verbose "buildCmdline: $buildCmdline"
        exec {
            Invoke-Expression $buildCmdline
        } 
    }
}

# Synopsis: Set the container image version
task GenerateContainerBuildTag Version,{

    if ($ContainerImageVersionOverride) {
        Write-Host "Overriding default container image version tag: $ContainerImageVersionOverride"
        $script:containerBuildTag = $ContainerImageVersionOverride
    }
    else {
        $script:containerBuildTag = ($script:GitVersion).SemVer
    }
}

# Synopsis: Publish Container Images to a Docker Registry
task PublishContainerImagesToDocker -If { $ContainerRegistryType -eq "docker" } GenerateContainerBuildTag,{

    if (!$ContainerRegistryPublishPrefix -and !$DockerRegistryUsername) {
        throw "Either 'ContainerRegistryPublishPrefix' or 'DockerRegistryUsername' must be defined when publishing to a Docker Registry"
    }

    foreach ($buildInfo in $ContainersToBuild) {
        $buildTag = "{0}:{1}" -f $buildInfo.ImageName.ToLower(), $containerBuildTag
        $publishTag = $ContainerRegistryPublishPrefix ? `
                            "$DockerRegistryFqdn/$ContainerRegistryPublishPrefix/$buildTag" : `
                            "$DockerRegistryFqdn/$DockerRegistryUsername/$buildTag"
        Write-Host "Publishing Container: $publishTag"
        exec {
            docker tag $buildTag $publishTag
            docker push $publishTag
        }
    }
}

# Synopsis: Publish Container Images to GHCR
task PublishContainerImagesToGhcr -If { $ContainerRegistryType -eq "ghcr" } EnsureGitHubCli,GenerateContainerBuildTag,{

    foreach ($buildInfo in $ContainersToBuild) {
        $buildTag = "{0}:{1}" -f $buildInfo.ImageName, $containerBuildTag
        $publishTag = $ContainerRegistryPublishPrefix ? `
                            "docker.pkg.github.com/$ContainerRegistryPublishPrefix/$buildTag" : `
                            "docker.pkg.github.com/$(gh repo view --json nameWithOwner | ConvertFrom-Json | Select-Object -ExpandProperty nameWithOwner)/$buildTag"
        Write-Host "Publishing Container: $publishTag"
        exec {
            docker tag $buildTag $publishTag
            docker push $publishTag
        }
    }
}

# Synopsis: Publish Container Images to Azure Container Registry
task PublishContainerImagesToAcr -If { $ContainerRegistryType -eq "acr" } GenerateContainerBuildTag,{

    if (!$ContainerRegistryFqdn) {
        throw "Missing value for 'ContainerRegistryFqdn' - this is required when publishing to Azure Container Registry"
    }

    if (!(Test-AzCliConnection)) {
        throw "You must be logged in to Azure CLI to publish to ACR"
    }

    if (!$UseAcrTasks) {
        Write-Host "Logging-in to ACR $ContainerRegistryFqdn $($AcrSubscription ? "(Subscription=$AcrSubscription)" : '') "
        exec { az acr login -n $ContainerRegistryFqdn $($AcrSubscription ? "--subscription $AcrSubscription" : "") }
    }

    foreach ($buildInfo in $ContainersToBuild) {
        $buildTag = "{0}:{1}" -f $buildInfo.ImageName, $containerBuildTag
        $publishTag = $ContainerRegistryPublishPrefix ? `
                            "$ContainerRegistryFqdn/$ContainerRegistryPublishPrefix/$buildTag" : `
                            "$ContainerRegistryFqdn/$buildTag"
        Write-Host "Publishing Container: $publishTag"

        if ($UseAcrTasks) {
            # Use 'acr import' command to re-tag the previously built image without the '--pre' suffix to reflect its final publication
            $sourceTag = "$publishTag--pre"
            $targetTag = $publishTag.Replace("$ContainerRegistryFqdn/", "")
            Write-Host "Promoting 'pre' container image:`n $sourceTag -> $targetTag"
            
            # Support cross-subscription ACR access ('--subscription' is not supported by 'az acr import' so we need to switch subscriptions temporarily)
            $currentSubscription = $null
            if ($AcrSubscription) {
                $currentSubscription = exec { az account show --query id --output tsv }
                Write-Host "Temporarily switching to ACR subscription: $currentSubscription -> $AcrSubscription"
                exec { az account set --subscription $AcrSubscription } | Out-Null
            }
            try {
                exec { & az acr import --name $ContainerRegistryFqdn --source $sourceTag --image $targetTag --force }
                exec { & az acr repository untag -n $ContainerRegistryFqdn --image "$targetTag--pre" }    
            }
            finally {
                if ($currentSubscription) {
                    Write-Host "Revert subscription: $AcrSubscription -> $currentSubscription"
                    exec { az account set --subscription $currentSubscription } | Out-Null
                }
            }
        }
        else {
            # Tag and push the image
            exec { & docker tag $buildTag $publishTag }
            exec { docker push $publishTag }
        }
    }
}

# Synopsis: Outputs the tag used for the built container images (e.g. local file, Build Server output variable)
task OutputContainerImageTagArtefact GenerateContainerBuildTag,{

    if ($containerBuildTag) {

        Write-Host "Creating build artefact to record the container image tag"
        Set-Content -Path $ContainerImageTagArtefactPath -Value $containerBuildTag

        Write-Host "ContainerImageTagArtefactPath: $ContainerImageTagArtefactPath"
        Write-Host "ContainerImageTag: $containerBuildTag"
    
        # Ensure we give a full path to the artefact/file
        $artefactFilePath = [IO.Path]::GetFullPath($ContainerImageTagArtefactPath)
        Write-Host "artefactFilePath: $artefactFilePath"

        if ($IsAzureDevops) {
            Write-Host "Sending container image tag details to Azure Pipelines..."
            Write-Host "##vso[task.uploadsummary]$artefactFilePath"
            Write-Host "##vso[task.setvariable variable=ContainerImageTagArtefactPath]$artefactFilePath"
            Write-Host "##vso[task.setvariable variable=ContainerImageTag]$containerBuildTag"
        }
        elseif ($IsGitHubActions) {
            Write-Host "Sending container image tag details to GitHub Actions..."
            "ContainerImageTagArtefactPath=$artefactFilePath" | Out-File -Encoding utf8 -Append $env:GITHUB_OUTPUT
            "ContainerImageTag=$containerBuildTag" | Out-File -Encoding utf8 -Append $env:GITHUB_OUTPUT
        }
    }
    else {
        Write-Warning "No 'image-tag' artefact was created due to the 'containerBuildTag' not being available."
    }
}

task PublishContainerImages -If { !$SkipPublishContainerImages } PublishContainerImagesToAcr,
                            PublishContainerImagesToGhcr,
                            PublishContainerImagesToDocker,
                            OutputContainerImageTagArtefact