diff --git a/.editorconfig b/.editorconfig
index f5b2e59c..a1409267 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,3 +13,6 @@ trim_trailing_whitespace = true
[Makefile]
indent_size = 4
indent_style = tab
+
+[Dockerfile]
+indent_size = 4
diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml
index 8e680927..8245a257 100644
--- a/.github/workflows/pipeline.yaml
+++ b/.github/workflows/pipeline.yaml
@@ -53,8 +53,9 @@ env:
GIT_WIN_VERSION: 2.47.0
# https://github.com/facebook/zstd/releases
ZSTD_WIN_VERSION: 1.5.6
- # https://www.python.org/downloads/windows
- PYTHON_VERSION: 3.12.7
+ # https://www.python.org/downloads
+ PYTHON_VERSION_MAJOR_MINOR: 3.12
+ PYTHON_VERSION_PATCH: 7
# https://nodejs.org/en/download/releases
NODE_VERSION: 20.18.0
# https://github.com/helm/helm/releases
@@ -114,9 +115,9 @@ jobs:
submodules: recursive
- name: SAST - Credentials
- uses: trufflesecurity/trufflehog@v3.75.0
+ uses: trufflesecurity/trufflehog@v3.82.11
with:
- base: ${{ github.event.repository.default_branch }}
+ base: main
extra_args: --only-verified
head: HEAD~1
@@ -126,12 +127,12 @@ jobs:
- init
- sast-creds
- sast-semgrep
- - static-test
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
+ # Required for running "helm" CLI
- name: Setup Helm
uses: azure/setup-helm@v4.2.0
with:
@@ -139,11 +140,14 @@ jobs:
# Required for running "npx" CLI
- name: Setup Node
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
+ # Required for running "cosign" CLI
- name: Setup Cosign
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
uses: sigstore/cosign-installer@v3.6.0
with:
cosign-release: v${{ env.COSIGN_VERSION }}
@@ -166,6 +170,8 @@ jobs:
src/helm/blue-agent
- name: Sign Helm chart
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
@@ -216,19 +222,20 @@ jobs:
snyk.sarif
- name: Upload results to GitHub Security
- uses: github/codeql-action/upload-sarif@v3.25.3
+ uses: github/codeql-action/upload-sarif@v3.26.13
continue-on-error: true
with:
sarif_file: merged.sarif
release-helm:
name: Release Helm chart
+ # Only deploy on non-scheduled main branch, as there is only one Helm repo and we cannot override an existing version
+ if: (github.event_name != 'schedule') && (github.ref == 'refs/heads/main')
needs:
+ - build-helm
- build-release-linux
- build-release-win
- - build-helm
- # Only deploy on non-scheduled main branch, as there is only one Helm repo and we cannot override an existing version
- if: (github.event_name != 'schedule') && (github.ref == 'refs/heads/main')
+ - static-test
runs-on: ubuntu-24.04
steps:
- name: Checkout
@@ -259,6 +266,9 @@ jobs:
static-test:
name: Static test
+ permissions:
+ contents: read
+ id-token: write
runs-on: ubuntu-24.04
steps:
- name: Checkout
@@ -266,7 +276,7 @@ jobs:
# Required for running "npx" CLI
- name: Setup Node
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
@@ -277,9 +287,11 @@ jobs:
hadolint --version
- name: Login to Azure
- uses: azure/login@v2.1.1
+ uses: azure/login@v2.2.0
with:
- creds: ${{ secrets.AZURE_CREDENTIALS }}
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- name: Run tests
run: |
@@ -291,7 +303,6 @@ jobs:
- init
- sast-creds
- sast-semgrep
- - static-test
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@@ -306,7 +317,9 @@ jobs:
- os: jammy
arch: linux/amd64,linux/arm64
- os: noble
- arch: linux/amd64,linux/arm64
+ # On GitHub Actions, build is too slow to be done on ARM64 virtialized on QEMU
+ # TODO: Re-enable ARM64 when GitHub Actions will provide native ARM64 runners
+ arch: linux/amd64
- os: ubi8
arch: linux/amd64,linux/arm64
- os: ubi9
@@ -315,15 +328,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.7
+ # Container build take a lot of disk space, there is no enough space on the runner to have both tools cache and container build
- name: Clean up disk
run: rm -rf /opt/hostedtoolcache
+ # Required to build multi-arch images
- name: Setup QEMU
id: setup-qemu
uses: docker/setup-qemu-action@v3.2.0
with:
platforms: ${{ matrix.arch }}
+ # Required for "docker build" command
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3.6.1
with:
@@ -333,11 +349,14 @@ jobs:
# Required for running "npx" CLI
- name: Setup Node
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
+ # Required for running "cosign" CLI
- name: Setup Cosign
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
uses: sigstore/cosign-installer@v3.6.0
with:
cosign-release: v${{ env.COSIGN_VERSION }}
@@ -359,7 +378,7 @@ jobs:
- name: Check if pre-release
id: prerelease
run: |
- if [ "${{ github.ref_name }}" == "${{ github.event.repository.default_branch }}" ]; then
+ if [ "${{ github.ref_name }}" == "main" ]; then
echo "prerelease=false" >> $GITHUB_OUTPUT
else
echo "prerelease=true" >> $GITHUB_OUTPUT
@@ -412,36 +431,47 @@ jobs:
GCLOUD_CLI_VERSION=${{ env.GCLOUD_CLI_VERSION }}
GO_VERSION=${{ env.GO_VERSION }}
POWERSHELL_VERSION=${{ env.POWERSHELL_VERSION }}
- PYTHON_VERSION=${{ env.PYTHON_VERSION }}
+ PYTHON_VERSION_MAJOR_MINOR=${{ env.PYTHON_VERSION_MAJOR_MINOR }}
+ PYTHON_VERSION_PATCH=${{ env.PYTHON_VERSION_PATCH }}
ROOTLESSKIT_VERSION=${{ env.ROOTLESSKIT_VERSION }}
TINI_VERSION=${{ env.TINI_VERSION }}
YQ_VERSION=${{ env.YQ_VERSION }}
- cache-from:
- ${{ env.CONTAINER_REGISTRY_GHCR }}/${{ env.CONTAINER_NAME }}:${{ matrix.os }}-${{ github.event.repository.default_branch }}-cache
- ${{ env.CONTAINER_REGISTRY_GHCR }}/${{ env.CONTAINER_NAME }}:${{ matrix.os }}-develop-cache
- ${{ steps.tag.outputs.tag }}-cache
- cache-to: ${{ steps.tag.outputs.tag }}-cache
+ cache-from: type=gha,scope=buildkit-${{ matrix.os }}-${{ github.ref_name }}
+ cache-to: type=gha,scope=buildkit-${{ matrix.os }}-${{ github.ref_name }}
context: src/docker
file: src/docker/Dockerfile-${{ matrix.os }}
labels: ${{ steps.meta.outputs.labels }}
+ outputs: type=registry,oci-mediatypes=true,compression=estargz,compression-level=9,force-compression=true
platforms: ${{ matrix.arch }}
provenance: true
- outputs: type=registry,oci-mediatypes=true,compression=estargz,compression-level=9,force-compression=true
sbom: true
tags: ${{ steps.meta.outputs.tags }}
+ # Cosign is voluntarily retried indefinitely to avoid breaking the build when the container registry is slow or throttle, with an exponential backoff delay with jitter
+ # See: https://github.com/clemlesne/blue-agent/issues/264
- name: Sign containers
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
while IFS= read -r tag; do
echo "Signing $tag"
- cosign sign \
- --key="env://COSIGN_PRIVATE_KEY" \
- --recursive \
- --yes \
- $tag
+ i=1
+ while true; do
+ cosign sign \
+ --key="env://COSIGN_PRIVATE_KEY" \
+ --recursive \
+ --yes \
+ $tag \
+ && break
+ jitter=$(python3 -c "import random; print(random.randint(-20, 20))")
+ backoff=$(python3 -c "import math; print(int(math.pow(3, $i) * (1 + $jitter / 100)))")
+ echo "retry: cosign returned $?, backing off for $backoff seconds and trying again ($i)..."
+ sleep $backoff
+ i=$((i + 1))
+ done
done <<< "${{ steps.meta.outputs.tags }}"
- name: Run SAST Snyk against containers
@@ -475,7 +505,7 @@ jobs:
*.sarif
- name: Upload results to GitHub Security
- uses: github/codeql-action/upload-sarif@v3.25.3
+ uses: github/codeql-action/upload-sarif@v3.26.13
continue-on-error: true
with:
sarif_file: merged.sarif
@@ -486,7 +516,6 @@ jobs:
- init
- sast-creds
- sast-semgrep
- - static-test
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
@@ -500,16 +529,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.7
+ # Container build take a lot of disk space, there is no enough space on the runner to have both tools cache and container build
- name: Clean up disk
run: Remove-Item -Path C:\hostedtoolcache -Recurse -Force
# Required for running "npx" CLI
- name: Setup Node
- uses: actions/setup-node@v4.0.2
+ uses: actions/setup-node@v4.0.4
with:
node-version: ${{ env.NODE_VERSION }}
+ # Required for running "cosign" CLI
- name: Setup Cosign
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
uses: sigstore/cosign-installer@v3.6.0
with:
cosign-release: v${{ env.COSIGN_VERSION }}
@@ -531,7 +564,7 @@ jobs:
- name: Check if pre-release
id: prerelease
run: |
- if ('${{ github.ref_name }}' -eq '${{ github.event.repository.default_branch }}') {
+ if ('${{ github.ref_name }}' -eq 'main') {
echo "prerelease=false" >> $env:GITHUB_OUTPUT
} else {
echo "prerelease=true" >> $env:GITHUB_OUTPUT
@@ -583,7 +616,8 @@ jobs:
"--build-arg", "GIT_VERSION=${{ env.GIT_WIN_VERSION }}",
"--build-arg", "JQ_VERSION=${{ env.JQ_WIN_VERSION }}",
"--build-arg", "POWERSHELL_VERSION=${{ env.POWERSHELL_VERSION }}",
- "--build-arg", "PYTHON_VERSION=${{ env.PYTHON_VERSION }}",
+ "--build-arg", "PYTHON_VERSION_MAJOR_MINOR=${{ env.PYTHON_VERSION_MAJOR_MINOR }}",
+ "--build-arg", "PYTHON_VERSION_PATCH=${{ env.PYTHON_VERSION_PATCH }}",
"--build-arg", "VS_BUILDTOOLS_VERSION=${{ env.VS_BUILDTOOLS_WIN_VERSION }}",
"--build-arg", "YQ_VERSION=${{ env.YQ_VERSION }}",
"--build-arg", "ZSTD_VERSION=${{ env.ZSTD_WIN_VERSION }}",
@@ -596,11 +630,6 @@ jobs:
$params += "--tag", $tag
}
- # Cache input
- $params += "--cache-from", "${{ env.CONTAINER_REGISTRY_GHCR }}/${{ env.CONTAINER_NAME }}:${{ matrix.os }}-${{ github.event.repository.default_branch }}"
- $params += "--cache-from", "${{ env.CONTAINER_REGISTRY_GHCR }}/${{ env.CONTAINER_NAME }}:${{ matrix.os }}-develop"
- $params += "--cache-from", "${{ steps.tag.outputs.tag }}"
-
$labels = ('${{ steps.meta.outputs.labels }}').Split([Environment]::NewLine)
foreach ($label in $labels) {
$params += "--label", $label
@@ -617,12 +646,6 @@ jobs:
}
Write-Host
- Write-Host "Pulling cache images:"
- foreach ($tag in $tags) {
- Write-Host " $tag"
- docker pull --quiet $tag || true
- }
-
Write-Host "Building"
docker build @params src\docker
@@ -631,7 +654,11 @@ jobs:
docker push --quiet $tag
}
+ # Cosign is voluntarily retried indefinitely to avoid breaking the build when the container registry is slow or throttle, with an exponential backoff delay with jitter
+ # See: https://github.com/clemlesne/blue-agent/issues/264
- name: Sign containers
+ # Only sign builds on main branch
+ if: github.ref == 'refs/heads/main'
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
@@ -639,11 +666,22 @@ jobs:
$tags = ('${{ steps.meta.outputs.tags }}').Split([Environment]::NewLine)
foreach ($tag in $tags) {
Write-Host "Signing $tag"
- cosign sign `
- --key="env://COSIGN_PRIVATE_KEY" `
- --recursive `
- --yes `
- $tag
+ $i = 1
+ while ($true) {
+ cosign sign `
+ --key="env://COSIGN_PRIVATE_KEY" `
+ --recursive `
+ --yes `
+ $tag
+ if ($?) {
+ break
+ }
+ $jitter = (Get-Random -Minimum -20 -Maximum 21) / 100
+ $backoff = [math]::Round([math]::Pow(3, $i) * (1 + $jitter))
+ Write-Host "retry: cosign returned $LASTEXITCODE, backing off for $backoff seconds and trying again ($i)..."
+ Start-Sleep -Seconds $backoff
+ $i++
+ }
}
- name: Run SAST Snyk against containers
@@ -670,7 +708,7 @@ jobs:
snyk.sarif
- name: Upload results to GitHub Security
- uses: github/codeql-action/upload-sarif@v3.25.3
+ uses: github/codeql-action/upload-sarif@v3.26.13
continue-on-error: true
with:
sarif_file: merged.sarif
@@ -692,7 +730,7 @@ jobs:
run: semgrep ci --sarif --output=semgrep.sarif
- name: Upload results to GitHub Security
- uses: github/codeql-action/upload-sarif@v3.25.3
+ uses: github/codeql-action/upload-sarif@v3.26.13
continue-on-error: true
with:
sarif_file: semgrep.sarif
@@ -706,6 +744,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.7
+ # Required for running "oras" CLI
- name: Setup ORAS
uses: oras-project/setup-oras@v1.2.0
with:
@@ -739,11 +778,12 @@ jobs:
update-docker-hub-description:
name: Update Docker Hub description
+ # Only deploy on non-scheduled main branch, as there is only one Helm repo and we cannot override an existing version
+ if: (github.event_name != 'schedule') && (github.ref == 'refs/heads/main')
needs:
- build-release-linux
- build-release-win
- # Only deploy on non-scheduled main branch, as there is only one Helm repo and we cannot override an existing version
- if: (github.event_name != 'schedule') && (github.ref == 'refs/heads/main')
+ - static-test
runs-on: ubuntu-24.04
steps:
- name: Checkout
@@ -760,17 +800,18 @@ jobs:
build-hugo:
name: Build Hugo site
- runs-on: ubuntu-24.04
needs:
- sast-creds
- sast-semgrep
- - static-test
+ runs-on: ubuntu-24.04
steps:
+ # Required for running "hugo" CLI
- name: Setup Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}/hugo_extended_${{ env.HUGO_VERSION }}_linux-amd64.deb
sudo dpkg -i ${{ runner.temp }}/hugo.deb
+ # Required as a Hugo build dependency
- name: Setup Dart Sass
run: sudo snap install dart-sass
@@ -808,6 +849,7 @@ jobs:
needs:
- build-hugo
- init
+ - static-test
# Only deploy on non-scheduled main branch, as there is only one Helm repo and we cannot override an existing version
if: (github.event_name != 'schedule') && (github.ref == 'refs/heads/main')
runs-on: ubuntu-24.04
@@ -842,20 +884,20 @@ jobs:
git push origin gh-pages
fi
- integration-test:
+ integration-test-linux:
name: Integration test (Linux ${{ matrix.os }})
- runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ id-token: write
needs:
+ - build-release-linux
- init
- - sast-creds
- - sast-semgrep
- static-test
- - build-release-linux
- - build-release-win
- concurrency: integration-test-${{ needs.init.outputs.BRANCH }}-${{ matrix.os }}
+ runs-on: ubuntu-24.04
+ concurrency: integration-test-linux-${{ needs.init.outputs.BRANCH }}-${{ matrix.os }}
strategy:
fail-fast: false
- # Rate limiting on Azure DevOps SaaS APIs is triggered quickluy by integration tests, so we need to limit the number of parallel jobs
+ # Rate limiting on Azure DevOps SaaS APIs is triggered quickly by integration tests, so we need to limit the number of parallel jobs
max-parallel: 3
matrix:
os: [bookworm, bullseye, focal, jammy, noble, ubi8, ubi9]
@@ -863,12 +905,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.7
+ # Required for running "sops" CLI
- name: Setup SOPS
run: |
curl -LO https://github.com/getsops/sops/releases/download/v${{ env.SOPS_VERSION }}/sops-v${{ env.SOPS_VERSION }}.linux.amd64
mv sops-v${{ env.SOPS_VERSION }}.linux.amd64 /usr/local/bin/sops
chmod +x /usr/local/bin/sops
+ # Configure local configuration encryption key
- name: Setup AGE key
run: |
age_folder="$XDG_CONFIG_HOME/sops/age"
@@ -876,9 +920,11 @@ jobs:
echo "${{ secrets.AGE_KEY }}" > ${age_folder}/keys.txt
- name: Login to Azure
- uses: azure/login@v2.1.1
+ uses: azure/login@v2.2.0
with:
- creds: ${{ secrets.AZURE_CREDENTIALS }}
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- name: Deploy Bicep
run: |
@@ -889,23 +935,31 @@ jobs:
- name: Integration
env:
- # Permissions: Agent Pools (Read); Build (Read & execute); Pipeline Resources (Use & manage); Project and Team (Read, write, & manage); Service Connections (Read, query, & manage)
+ # Permissions: agent pools (read & manage); build (read & execute); pipeline resources (use & manage); project and team (read, write, & manage); service connections (read, query, & manage)
# Recommended group membership: Project Collection Build Service Accounts
AZURE_DEVOPS_EXT_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
# Scope: clemlesne/blue-agent
- # Permissions: Contents (read-only); Metadata (read-only); Webhooks (read & write)
+ # Permissions: contents (read-only); metadata (read-only); webhooks (read & write)
AZURE_DEVOPS_EXT_GITHUB_PAT: ${{ secrets.AZURE_DEVOPS_GITHUB_PAT }}
# Script wait indefinitely for external events, so we need to timeout
timeout-minutes: 30
run: |
- make integration \
+ make integration-run \
flavor="${{ matrix.os }}" \
prefix="${{ needs.init.outputs.BRANCH }}" \
version="sha-$(git rev-parse --short HEAD)"
- # - name: Cleanup
- # if: always()
- # run: |
- # make destroy-bicep \
- # flavor="${{ matrix.os }}" \
- # prefix="${{ needs.init.outputs.BRANCH }}"
+ - name: Cleanup
+ if: always()
+ env:
+ # Permissions: agent pools (read & manage); build (read & execute); pipeline resources (use & manage); project and team (read, write, & manage); service connections (read, query, & manage)
+ # Recommended group membership: Project Collection Build Service Accounts
+ AZURE_DEVOPS_EXT_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
+ run: |
+ make integration-cleanup \
+ flavor="${{ matrix.os }}" \
+ prefix="${{ needs.init.outputs.BRANCH }}"
+
+ # make destroy-bicep \
+ # flavor="${{ matrix.os }}" \
+ # prefix="${{ needs.init.outputs.BRANCH }}"
diff --git a/.gitignore b/.gitignore
index 28d277f7..0f7059b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
-# Created by https://www.toptal.com/developers/gitignore/api/osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode
-# Edit at https://www.toptal.com/developers/gitignore?templates=osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode
+# Created by https://www.toptal.com/developers/gitignore/api/osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode,dotenv
+# Edit at https://www.toptal.com/developers/gitignore?templates=osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode,dotenv
+
+### dotenv ###
+.env
### Eclipse ###
.metadata
@@ -548,4 +551,4 @@ FodyWeavers.xsd
### VisualStudio Patch ###
# Additional files built by Visual Studio
-# End of https://www.toptal.com/developers/gitignore/api/osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode
+# End of https://www.toptal.com/developers/gitignore/api/osx,helm,linux,eclipse,windows,visualstudio,visualstudiocode,dotenv
diff --git a/Makefile b/Makefile
index d99ccdb8..fda2ad70 100644
--- a/Makefile
+++ b/Makefile
@@ -4,11 +4,16 @@
flavor ?= null
version ?= null
# Dynamic parameters
-prefix ?= $(shell hostname | tr '[:upper:]' '[:lower:]' | tr '.' '-')
+prefix ?= $(shell hostname | tr "[:upper:]" "[:lower:]" | tr "." "-")
deployment_name ?= $(prefix)-$(flavor)
# Deployment outputs
-job_name ?= $(shell az deployment sub show --name '$(deployment_name)' | yq '.properties.outputs["jobName"].value')
-rg_name ?= $(shell az deployment sub show --name '$(deployment_name)' | yq '.properties.outputs["rgName"].value')
+bicep_outputs ?= $(shell az deployment sub show --name "$(deployment_name)" | yq '.properties.outputs')
+job_name ?= $(shell echo $(bicep_outputs) | yq '.jobName.value')
+rg_name ?= $(shell echo $(bicep_outputs) | yq '.rgName.value')
+# Container App Job environment
+container_specs ?= $(shell az containerapp job show --name "$(job_name)" --resource-group "$(rg_name)" | yq '.properties.template.containers[0]')
+job_image ?= $(shell echo $(container_specs) | yq '.image')
+job_env ?= $(shell echo $(container_specs) | yq '.env | map("\(.name)=\(.value // \"secretref:\" + .secretRef)") | .[]')
test:
@echo "➡️ Running Prettier"
@@ -38,6 +43,14 @@ lint:
--verbose
deploy-bicep:
+ $(MAKE) deploy-bicep-iac
+
+ @echo "⏳ Wait for the Bicep output to be available"
+ sleep 10
+
+ $(MAKE) deploy-bicep-template
+
+deploy-bicep-iac:
@echo "➡️ Decrypting Bicep parameters"
sops -d test/bicep/test.enc.json > test/bicep/test.json
@@ -55,19 +68,29 @@ deploy-bicep:
@echo "➡️ Cleaning up Bicep parameters"
rm test/bicep/test.json
- @echo "➡️ Starting init job"
+deploy-bicep-template:
+ @echo "➡️ Starting template job"
az containerapp job start \
+ --env-vars $(job_env) AZP_TEMPLATE_JOB=1 \
+ --image $(job_image) \
--name $(job_name) \
--resource-group $(rg_name)
destroy-bicep:
- @echo "➡️ Destroying"
+ @echo "➡️ Destroying Azure resources"
az group delete \
--name "$(rg_name)" \
--yes
integration:
- @bash test/integration.sh $(prefix) $(flavor) $(version) $(job_name)
+ $(MAKE) integration-run
+ $(MAKE) integration-cleanup
+
+integration-run:
+ @bash test/integration-run.sh $(prefix) $(flavor) $(version) $(job_name) $(rg_name) github-actions
+
+integration-cleanup:
+ @bash test/integration-cleanup.sh $(job_name) github-actions
docs:
cd docs && hugo server
diff --git a/cicd/docker-build-local.sh b/cicd/docker-build-local.sh
index eed86b5f..04e6a461 100644
--- a/cicd/docker-build-local.sh
+++ b/cicd/docker-build-local.sh
@@ -48,13 +48,15 @@ for suffix in ${SUFFIXES}; do
--build-arg "GO_VERSION=${GO_VERSION}" \
--build-arg "JQ_VERSION=${JQ_VERSION}" \
--build-arg "POWERSHELL_VERSION=${POWERSHELL_VERSION}" \
- --build-arg "PYTHON_VERSION=${PYTHON_VERSION}" \
+ --build-arg "PYTHON_VERSION_MAJOR_MINOR=${PYTHON_VERSION_MAJOR_MINOR}" \
+ --build-arg "PYTHON_VERSION_PATCH=${PYTHON_VERSION_PATCH}" \
--build-arg "ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}" \
--build-arg "TINI_VERSION=${TINI_VERSION}" \
--build-arg "VS_BUILDTOOLS_VERSION=${VS_BUILDTOOLS_WIN_VERSION}" \
--build-arg "YQ_VERSION=${YQ_VERSION}" \
--build-arg "ZSTD_VERSION=${ZSTD_WIN_VERSION}" \
- --tag $tag \
--file ${PREFIX}${suffix} \
+ --platform linux/amd64,linux/arm64 \
+ --tag $tag \
$FOLDER
done
diff --git a/cicd/env-github-actions.sh b/cicd/env-github-actions.sh
index daf157f0..384db347 100644
--- a/cicd/env-github-actions.sh
+++ b/cicd/env-github-actions.sh
@@ -17,29 +17,13 @@ if [ ! -f "$PIPELINE_FILE" ]; then
fi
# Get "env" property from pipeline file
-ENV=$(yq eval '.env' $PIPELINE_FILE)
+ENV=$(yq '.env | to_entries | map("\(.key)=\(.value)") | .[]' $PIPELINE_FILE)
# Store all properties from ENV, in environment variables
while IFS= read -r line; do
- # Skip empty lines
- if [ -z "$line" ]; then
- continue
- fi
-
- # Skip comments
- if [[ $line == \#* ]]; then
- continue
- fi
-
- # Split the line into key and value, based on ":"
- key=$(echo $line | cut -d':' -f1)
- value=$(echo $line | cut -d':' -f2-)
-
- # Trim the values
- key=$(echo $key | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
- value=$(echo $value | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
-
+ # Remove double quotes
+ line=$(echo $line | sed 's/\"//g')
# Set the environment variable
- echo "From GitHub Actions: $key=$value"
- export "$key"="$value"
+ echo "From GitHub Actions: $line"
+ export "$line"
done <<< "$ENV"
diff --git a/docs/content/docs/advanced-topics/_index.md b/docs/content/docs/advanced-topics/_index.md
index 9ed225c7..be2b162c 100644
--- a/docs/content/docs/advanced-topics/_index.md
+++ b/docs/content/docs/advanced-topics/_index.md
@@ -15,6 +15,7 @@ Explore the following sections to learn how to use Blue Agent:
{{< card link="docker-in-docker" title="Build container images" icon="archive" >}}
{{< card link="helm-values" title="Helm values" icon="adjustments" >}}
{{< card link="performance" title="Performance" icon="chart-bar" >}}
+{{< card link="pricing" title="Pricing" icon="currency-dollar" >}}
{{< card link="provided-software" title="Provided software" icon="cube" >}}
{{< card link="proxy" title="Proxy" icon="shield-check" >}}
{{< /cards >}}
diff --git a/docs/content/docs/advanced-topics/bicep-deployment.md b/docs/content/docs/advanced-topics/bicep-deployment.md
index 5f5f252a..e143716c 100644
--- a/docs/content/docs/advanced-topics/bicep-deployment.md
+++ b/docs/content/docs/advanced-topics/bicep-deployment.md
@@ -6,21 +6,22 @@ Bicep is a deployment language for Azure, allowing to easily deploy resources on
#### Bicep parameters
-| Parameter | Description | Default |
-| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
-| `autoscalingMaxReplicas` | Maximum number of simultaneous jobs the agent can run | `100` |
-| `autoscalingMinReplicas` | Minimum number of replicas the agent should have | `0` |
-| `extraEnv` | Extra environment variables to pass to the agent | `[]` |
-| `imageFlavor` | Flavor of the container image, represents the Linux distribution. Allowed values: `bookworm`, `bullseye`, `focal`, `jammy`, `ubi8`, `ubi9` | `bookworm` |
-| `imageName` | Name of the container image | `clemlesne/blue-agent` |
-| `imageRegistry` | Registry of the container image. Allowed values: `docker.io`, `ghcr.io` | `ghcr.io` |
-| `imageVersion` | Version of the container image, it is recommended to use a specific version like "1.0.0" instead of "latest" | `main` |
-| `instance` | Name of the instance, will be used to build the name of the resources | Value from `deployment().name` |
-| `location` | Location of resources | `westeurope` |
-| `pipelinesCapabilities` | Capabilities of the agent | `['arch_x64']` |
-| `pipelinesOrganizationURL` | URL of the Azure DevOps organization | _None_ |
-| `pipelinesPersonalAccessToken` | Personal access token allowing the agent to connect to the Azure DevOps organization. This parameter is secure. | _None_ |
-| `pipelinesPoolName` | Name of the Azure Pipelines self-hosted pool the agent should be added to | _None_ |
-| `pipelinesTimeout` | Timeout in seconds for the agent to run a job before it is automatically terminated | `3600` |
-| `resourcesCpu` | Number of CPU cores allocated to the agent | `2` |
-| `resourcesMemory` | Amount of memory allocated to the agent | `4Gi` |
+| Parameter | Description | Default |
+| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
+| `autoscalingMaxReplicas` | Maximum number of simultaneous jobs the agent can run | `100` |
+| `autoscalingMinReplicas` | Minimum number of replicas the agent should have | `0` |
+| `autoscalingPollingInterval` | Minimum number of replicas the agent should have; Warning, a low value will cause rate limiting or throttling, and can cause high load on the Azure DevOps API | `10` |
+| `extraEnv` | Extra environment variables to pass to the agent | `[]` |
+| `imageFlavor` | Flavor of the container image, represents the Linux distribution. Allowed values: `bookworm`, `bullseye`, `focal`, `jammy`, `noble`, `ubi8`, `ubi9` | `bookworm` |
+| `imageName` | Name of the container image | `clemlesne/blue-agent` |
+| `imageRegistry` | Registry of the container image. Allowed values: `docker.io`, `ghcr.io` | `ghcr.io` |
+| `imageVersion` | Version of the container image, it is recommended to use a specific version like "1.0.0" instead of "latest" | `main` |
+| `instance` | Name of the instance, will be used to build the name of the resources | Value from `deployment().name` |
+| `location` | Location of resources | `westeurope` |
+| `pipelinesCapabilities` | Capabilities of the agent | `['arch_x64']` |
+| `pipelinesOrganizationURL` | URL of the Azure DevOps organization | _None_ |
+| `pipelinesPersonalAccessToken` | Personal access token allowing the agent to connect to the Azure DevOps organization. This parameter is secure. | _None_ |
+| `pipelinesPoolName` | Name of the Azure Pipelines self-hosted pool the agent should be added to | _None_ |
+| `pipelinesTimeout` | Timeout in seconds for the agent to run a job before it is automatically terminated | `3600` |
+| `resourcesCpu` | Number of CPU cores allocated to the agent | `2` |
+| `resourcesMemory` | Amount of memory allocated to the agent | `4Gi` |
diff --git a/docs/content/docs/advanced-topics/docker-in-docker.md b/docs/content/docs/advanced-topics/docker-in-docker.md
index 56cf19da..056f4c8a 100644
--- a/docs/content/docs/advanced-topics/docker-in-docker.md
+++ b/docs/content/docs/advanced-topics/docker-in-docker.md
@@ -21,6 +21,7 @@ Linux systems are supported, but not Windows:
| `ghcr.io/clemlesne/blue-agent:bullseye-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:focal-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:jammy-main` | ✅ |
+| `ghcr.io/clemlesne/blue-agent:noble-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:ubi8-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:ubi9-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:win-ltsc2019-main` | ❌ |
diff --git a/docs/content/docs/advanced-topics/helm-deployment.md b/docs/content/docs/advanced-topics/helm-deployment.md
index da2afa37..6ac2bd41 100644
--- a/docs/content/docs/advanced-topics/helm-deployment.md
+++ b/docs/content/docs/advanced-topics/helm-deployment.md
@@ -14,14 +14,16 @@ Helm is a package manager for Kubernetes, allowing to easily deploy applications
| `annotations` | Add custom annotations to the Pod | `{}` |
| `autoscaling.cooldown` | Time in seconds the automation will wait until there is no more pipeline asking for an agent; same delay is then applied for system termination | `60` |
| `autoscaling.enabled` | Enable the auto-scaling; requires [KEDA](https://keda.sh), but can be started without; Be warning, disabling auto-scaling implies a shutdown of the existing agents during a Helm instance upgrade, according to `pipelines.timeout` | `true` |
+| `autoscaling.minReplicas` | Minimum number of replicas | `0` |
| `autoscaling.maxReplicas` | Maximum number of pods, remaining jobs will be kept in queue; the default value is arbitrary, to avoid misconfiguration | `100` |
+| `autoscaling.pollingInterval` | Interval in seconds to poll for new jobs; Warning, a low value will cause rate limiting or throttling, and can cause high load on the Azure DevOps API | `10` |
| `extraEnv` | Additional environment variables for the agent container | `[]` |
| `extraManifests` | Extra manifests to deploy as an array | `[]` |
| `extraNodeSelectors` | Additional node labels for pod assignment | `{}` |
| `extraVolumeMounts` | Additional volume mounts for the agent container | `[]` |
| `extraVolumes` | Additional volumes for the agent pod | `[]` |
| `fullnameOverride` | Overrides release fullname | `""` |
-| `image.flavor` | Container image tag, can be `bookworm`, `bullseye`, `focal`, `jammy`, `ubi8`, `ubi9`, `win-ltsc2019`, or `win-ltsc2022` | `bookworm` |
+| `image.flavor` | Container image tag, can be `bookworm`, `bullseye`, `focal`, `jammy`, `noble`, `ubi8`, `ubi9`, `win-ltsc2019`, or `win-ltsc2022` | `bookworm` |
| `image.isWindows` | Turn on is the agent is a Windows-based system | `false` |
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
| `image.repository` | Container image repository | `ghcr.io/clemlesne/blue-agent:bullseye` |
@@ -46,7 +48,7 @@ Helm is a package manager for Kubernetes, allowing to easily deploy applications
| `podSecurityContext` | Security rules applied to the Pod ([more details](https://kubernetes.io/docs/concepts/security/pod-security-standards)) | `{}` |
| `replicaCount` | Default fixed amount of agents deployed; those are not auto-scaled | `3` |
| `resources` | Resource limits | `{ "resources": { "limits": { "cpu": 2, "memory": "4Gi", "ephemeral-storage": "8Gi" }, "requests": { "cpu": 1, "memory": "2Gi", "ephemeral-storage": "2Gi" }}}` |
-| `revisionHistoryLimit` | Number of revisions to keep in the history of the Deployment | `10` |
+| `revisionHistoryLimit` | Number of revisions to keep in the history of the Deployment; Warning, [setting this to 0](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy) will disable the Deployment history and cause inability to rollback | `10` |
| `secret.create` | Create Secret, must contains `personalAccessToken` and `organizationURL` variables | `true` |
| `secret.name` | Secret name | _Release name_ |
| `securityContext` | Security rules applied to the container ([more details](https://kubernetes.io/docs/concepts/security/pod-security-standards)) | `{}` |
diff --git a/docs/content/docs/advanced-topics/pricing.md b/docs/content/docs/advanced-topics/pricing.md
new file mode 100644
index 00000000..107d9fee
--- /dev/null
+++ b/docs/content/docs/advanced-topics/pricing.md
@@ -0,0 +1,35 @@
+---
+title: Pricing
+---
+
+Blue Agent packages infrastructure as code ready to be deployed on two stacks, Kubernetes and Azure Container Apps.
+
+#### Kubernetes
+
+Costs of a Kubernetes cluster vary a lot depending of your provider, region, and the resources you need.
+
+For precise cost reports, you can use [OpenCost](https://github.com/opencost/opencost), or [Azure Kubernetes Service cost analysis addon](https://learn.microsoft.com/en-us/azure/aks/cost-analysis). The AKS cost analysis addon is based on OpenCost.
+
+#### Azure Container Apps
+
+As of Oct 22, 2024, Azure Container Apps pricing is as follows:
+
+| Meter | Pay as you go Price\* | 1-year Savings Plan Price\* | 3-year Savings Plan Price\* |
+| -------------------- | --------------------- | -------------------------------- | -------------------------------- |
+| vCPU (seconds) | $0.000024 /sec | $0.0000204 /sec
~15% savings | $0.00001992 /sec
~17% savings |
+| Memory (GiB-Seconds) | $0.000003 /sec | $0.00000255 /sec
~15% savings | $0.00000249 /sec
~17% savings |
+
+As agents are scaled down to zero when not in use, the cost is calculated based on the time the agent is running. Default deployment is 2 vCPUs and 4GiB of memory.
+
+```txt
+= ([vCPU cost] * 2 + [Memory cost] * 4) * [Time in seconds]
+= (0.000024 * 2 + 0.000003 * 4) * [Time in seconds]
+= 0.00006 * [Time in seconds]
+```
+
+Thus, cost per hour is:
+
+```txt
+= 0.00006 * 3600
+= $0.216
+```
diff --git a/docs/content/docs/advanced-topics/provided-software.md b/docs/content/docs/advanced-topics/provided-software.md
index 16e9ed17..47c1586a 100644
--- a/docs/content/docs/advanced-topics/provided-software.md
+++ b/docs/content/docs/advanced-topics/provided-software.md
@@ -20,10 +20,13 @@ Softwares are operating system specific. The following table lists the softwares
- [ASP.NET Core Runtime](https://github.com/dotnet/aspnetcore)
- [Python 3.12](https://docs.python.org/3/whatsnew/3.12.html)
- Tools
+ - [dnsutils](https://www.isc.org/bind)
- [git](https://github.com/git-for-windows/git)
- [gzip](https://www.gnu.org/software/gzip)
- [jq](https://github.com/stedolan/jq)
- [make](https://www.gnu.org/software/make)
+ - [openssl](https://www.openssl.org)
+ - [rsync](https://rsync.samba.org)
- [tar](https://www.gnu.org/software/tar)
- [unzip](https://infozip.sourceforge.net/UnZip.html)
- [wget](https://www.gnu.org/software/wget)
diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md
index ae9f7f1f..63e7cdbd 100644
--- a/docs/content/docs/getting-started.md
+++ b/docs/content/docs/getting-started.md
@@ -11,7 +11,7 @@ weight: 1
### Prepare the Azure DevOps organization
-Create [a new agent pool](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues) in Azure DevOps. Then, create [the personal access token](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/personal-access-token-agent-registration?view=azure-devops), with the scope `Agent Pools (read & manage)`, allowing access from the agent to Azure DevOps.
+Create [a new agent pool](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues) in Azure DevOps. Then, create [the personal access token](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/personal-access-token-agent-registration?view=azure-devops), with the scope `agent pools (read & manage)`, allowing access from the agent to Azure DevOps.
### Deploy
@@ -86,7 +86,7 @@ OS support is generally called "flavor" in this documentation. The following tab
| ------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ghcr.io/clemlesne/blue-agent:bookworm-main` | [Debian Bookworm (12)](https://www.debian.org/releases/bookworm) slim | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/bookworm-main?label=) | `amd64`, `arm64/v8` | [See Debian LTS wiki.](https://wiki.debian.org/LTS) |
| `ghcr.io/clemlesne/blue-agent:bullseye-main` | [Debian Bullseye (11)](https://www.debian.org/releases/bullseye) slim | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/bullseye-main?label=) | `amd64`, `arm64/v8` | [See Debian LTS wiki.](https://wiki.debian.org/LTS) |
-| `ghcr.io/clemlesne/blue-agent:noble-main` | [Ubuntu Noble (24.04)](https://www.releases.ubuntu.com/noble) minimal | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/noble-main?label=) | `amd64`, `arm64/v8` | [See Ubuntu LTS wiki.](https://wiki.ubuntu.com/Releases) |
+| `ghcr.io/clemlesne/blue-agent:noble-main` | [Ubuntu Noble (24.04)](https://www.releases.ubuntu.com/noble) minimal | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/noble-main?label=) | `amd64` | [See Ubuntu LTS wiki.](https://wiki.ubuntu.com/Releases) |
| `ghcr.io/clemlesne/blue-agent:jammy-main` | [Ubuntu Jammy (22.04)](https://www.releases.ubuntu.com/jammy) minimal | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/jammy-main?label=) | `amd64`, `arm64/v8` | [See Ubuntu LTS wiki.](https://wiki.ubuntu.com/Releases) |
| `ghcr.io/clemlesne/blue-agent:focal-main` | [Ubuntu Focal (20.04)](https://www.releases.ubuntu.com/focal) minimal | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/focal-main?label=) | `amd64`, `arm64/v8` | [See Ubuntu LTS wiki.](https://wiki.ubuntu.com/Releases) |
| `ghcr.io/clemlesne/blue-agent:ubi9-main` | [Red Hat UBI 9](https://developers.redhat.com/articles/ubi-faq) minimal | ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/clemlesne/blue-agent/ubi9-main?label=) | `amd64`, `arm64/v8` | [See Red Hat product life cycles.](https://access.redhat.com/product-life-cycles/?product=Red%20Hat%20Enterprise%20Linux) |
diff --git a/docs/content/docs/security.md b/docs/content/docs/security.md
index 57d81cb6..e664b7ed 100644
--- a/docs/content/docs/security.md
+++ b/docs/content/docs/security.md
@@ -18,6 +18,7 @@ Scanned systems:
| `ghcr.io/clemlesne/blue-agent:bullseye-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:focal-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:jammy-main` | ✅ |
+| `ghcr.io/clemlesne/blue-agent:noble-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:ubi8-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:ubi9-main` | ✅ |
| `ghcr.io/clemlesne/blue-agent:win-ltsc2019-main` | ✅ |
diff --git a/docs/themes/hextra b/docs/themes/hextra
index c6de4b5b..37089d23 160000
--- a/docs/themes/hextra
+++ b/docs/themes/hextra
@@ -1 +1 @@
-Subproject commit c6de4b5b6b1ec04647b0235e9c8b1158b1d58c09
+Subproject commit 37089d237ae8ce5f9ea807a1089e8ca3d5043ff7
diff --git a/src/bicep/agent.bicep b/src/bicep/agent.bicep
index f96bb9b7..ccdd860a 100644
--- a/src/bicep/agent.bicep
+++ b/src/bicep/agent.bicep
@@ -1,5 +1,6 @@
param autoscalingMaxReplicas int
param autoscalingMinReplicas int
+param autoscalingPollingInterval int
param extraEnv array
param imageFlavor string
param imageName string
@@ -85,7 +86,7 @@ resource job 'Microsoft.App/jobs@2023-11-02-preview' = {
scale: {
maxExecutions: autoscalingMaxReplicas
minExecutions: autoscalingMinReplicas
- pollingInterval: 15
+ pollingInterval: autoscalingPollingInterval
rules: [
{
name: 'azure-pipelines'
diff --git a/src/bicep/main.bicep b/src/bicep/main.bicep
index 5107b960..8e53fff3 100644
--- a/src/bicep/main.bicep
+++ b/src/bicep/main.bicep
@@ -4,6 +4,9 @@ param autoscalingMaxReplicas int = 100
@description('Minimum number of replicas the agent should have')
@minValue(0)
param autoscalingMinReplicas int = 0
+@description('Interval in seconds to poll for new jobs; Warning, a low value will cause rate limiting or throttling, and can cause high load on the Azure DevOps API')
+@minValue(1)
+param autoscalingPollingInterval int = 10
@description('Extra environment variables to pass to the agent')
param extraEnv array = []
@description('Flavor of the container image, represents the Linux distribution')
@@ -75,6 +78,7 @@ module agent 'agent.bicep' = {
params: {
autoscalingMaxReplicas: autoscalingMaxReplicas
autoscalingMinReplicas: autoscalingMinReplicas
+ autoscalingPollingInterval: autoscalingPollingInterval
extraEnv: extraEnv
imageFlavor: imageFlavor
imageName: imageName
diff --git a/src/docker/Dockerfile-bookworm b/src/docker/Dockerfile-bookworm
index d1d8b0e4..6df45697 100644
--- a/src/docker/Dockerfile-bookworm
+++ b/src/docker/Dockerfile-bookworm
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim@sha256:b3cdb99fb356091b6395f3444d355da8ae5d63572ba777bed95b65848d6e02be AS base
# Force apt-get to not use TTY
@@ -17,11 +20,11 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - dbus-user-session, fuse-overlayfs, iptables, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
build-essential \
@@ -29,6 +32,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
cargo \
curl \
dbus-user-session \
+ dnsutils \
fuse-overlayfs \
git \
git-lfs \
@@ -42,6 +46,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
lsb-release \
make \
pkg-config \
+ rsync \
software-properties-common \
sudo \
tar \
@@ -58,6 +63,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -71,19 +81,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
g++ \
@@ -101,48 +116,56 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
libxmlsec1-dev \
lzma \
lzma-dev \
- tk-dev \
uuid-dev \
xz-utils \
zlib1g-dev \
&& curl -LsSf --retry 8 --retry-all-errors https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(dpkg-buildflags --get CFLAGS)" \
+ && ldflags="$(dpkg-buildflags --get LDFLAGS)" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-bullseye b/src/docker/Dockerfile-bullseye
index 8ba1981e..6582cd34 100644
--- a/src/docker/Dockerfile-bullseye
+++ b/src/docker/Dockerfile-bullseye
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim@sha256:3717ce4bc6e34336ac100762eb766dc9cb739543686d0189001c1cafa57ba29c AS base
# Force apt-get to not use TTY
@@ -14,11 +17,11 @@ ENV PYTHONDONTWRITEBYTECODE=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - dbus-user-session, fuse-overlayfs, iptables, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
build-essential \
@@ -26,6 +29,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
cargo \
curl \
dbus-user-session \
+ dnsutils \
fuse-overlayfs \
git \
git-lfs \
@@ -39,6 +43,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
lsb-release \
make \
pkg-config \
+ rsync \
software-properties-common \
sudo \
tar \
@@ -55,6 +60,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -68,19 +78,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
g++ \
@@ -99,48 +114,56 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
libxmlsec1-dev \
lzma \
lzma-dev \
- tk-dev \
uuid-dev \
xz-utils \
zlib1g-dev \
&& curl -LsSf --retry 8 --retry-all-errors https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(dpkg-buildflags --get CFLAGS)" \
+ && ldflags="$(dpkg-buildflags --get LDFLAGS)" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-focal b/src/docker/Dockerfile-focal
index 48e8c1cd..9d7ab409 100644
--- a/src/docker/Dockerfile-focal
+++ b/src/docker/Dockerfile-focal
@@ -1,4 +1,7 @@
-FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal@sha256:85452c433603c39424da21d775002072bf6d9e5ec9ae3e4e62f5b8f3046200c5 AS base
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
+FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal@sha256:fe64a7f5bf2e300e52ad4eadc8d59c0ec7f096e22107d910c478366ee99c903d AS base
# Force apt-get to not use TTY
ENV DEBIAN_FRONTEND=noninteractive
@@ -14,11 +17,11 @@ ENV PYTHONDONTWRITEBYTECODE=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - dbus-user-session, iptables, uidmap, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
build-essential \
@@ -26,6 +29,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
cargo \
curl \
dbus-user-session \
+ dnsutils \
git \
git-lfs \
gnupg \
@@ -38,6 +42,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
lsb-release \
make \
pkg-config \
+ rsync \
software-properties-common \
sudo \
tar \
@@ -54,6 +59,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -67,19 +77,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
g++ \
@@ -98,48 +113,56 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
libxmlsec1-dev \
lzma \
lzma-dev \
- tk-dev \
uuid-dev \
xz-utils \
zlib1g-dev \
&& curl -LsSf --retry 8 https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(dpkg-buildflags --get CFLAGS)" \
+ && ldflags="$(dpkg-buildflags --get LDFLAGS)" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-jammy b/src/docker/Dockerfile-jammy
index e99acdb6..55ef39ff 100644
--- a/src/docker/Dockerfile-jammy
+++ b/src/docker/Dockerfile-jammy
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy@sha256:d41af821cc90286d7c0d81c6a25733846ee7eebb2b55479934af909afd36471a AS base
# Force apt-get to not use TTY
@@ -14,11 +17,11 @@ ENV PYTHONDONTWRITEBYTECODE=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - dbus-user-session, iptables, uidmap, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
build-essential \
@@ -26,6 +29,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
cargo \
curl \
dbus-user-session \
+ dnsutils \
git \
git-lfs \
gnupg \
@@ -38,6 +42,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
lsb-release \
make \
pkg-config \
+ rsync \
software-properties-common \
sudo \
tar \
@@ -54,6 +59,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -67,19 +77,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
g++ \
@@ -98,48 +113,56 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
libxmlsec1-dev \
lzma \
lzma-dev \
- tk-dev \
uuid-dev \
xz-utils \
zlib1g-dev \
&& curl -LsSf --retry 8 --retry-all-errors https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(dpkg-buildflags --get CFLAGS)" \
+ && ldflags="$(dpkg-buildflags --get LDFLAGS)" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-noble b/src/docker/Dockerfile-noble
index c7cdcb4a..b46f8b86 100644
--- a/src/docker/Dockerfile-noble
+++ b/src/docker/Dockerfile-noble
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble@sha256:a516b80935ab07dc415244dcdb8c52f4592644282127ecfa37c77561d26d25d5 AS base
# Force apt-get to not use TTY
@@ -17,11 +20,11 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - dbus-user-session, fuse-overlayfs, iptables, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
build-essential \
@@ -29,6 +32,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
cargo \
curl \
dbus-user-session \
+ dnsutils \
fuse-overlayfs \
git \
git-lfs \
@@ -42,6 +46,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
lsb-release \
make \
pkg-config \
+ rsync \
software-properties-common \
sudo \
tar \
@@ -58,6 +63,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -71,19 +81,22 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
-ARG PYTHON_VERSION
-ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/var/cache,type=cache,sharing=locked \
+# Build Python from source, then verify installation
+RUN --mount=target=/var/lib/apt/lists,type=cache,id=apt-lists-${TARGETPLATFORM},sharing=locked --mount=target=/var/cache,type=cache,id=var-cache-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
apt-get update -q \
&& apt-get install -y -q --no-install-recommends \
g++ \
@@ -101,48 +114,56 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked --mount=target=/
libxmlsec1-dev \
lzma \
lzma-dev \
- tk-dev \
uuid-dev \
xz-utils \
zlib1g-dev \
&& curl -LsSf --retry 8 --retry-all-errors https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(dpkg-buildflags --get CFLAGS)" \
+ && ldflags="$(dpkg-buildflags --get LDFLAGS)" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-ubi8 b/src/docker/Dockerfile-ubi8
index 9fd1334f..9d28ccfd 100644
--- a/src/docker/Dockerfile-ubi8
+++ b/src/docker/Dockerfile-ubi8
@@ -1,4 +1,7 @@
-FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10@sha256:a47c89f02b39a98290f88204ed3d162845db0a0c464b319c2596cfd1e94b444e AS base
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10@sha256:7583ca0ea52001562bd81a961da3f75222209e6192e4e413ee226cff97dbd48c AS base
# Configure local user
ENV USER=root
@@ -12,11 +15,12 @@ ENV PYTHONDONTWRITEBYTECODE=1
# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - fuse-overlayfs, iptables, shadow-utils, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
-RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
+RUN --mount=target=/var/cache/yum,type=cache,id=yum-${TARGETPLATFORM},sharing=locked \
microdnf install -y --nodocs --setopt=install_weak_deps=0 \
aspnetcore-runtime-8.0 \
+ bind-utils \
ca-certificates \
cargo \
curl \
@@ -36,6 +40,7 @@ RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
openssl \
openssl-devel \
pkg-config \
+ rsync \
shadow-utils \
sudo \
tar \
@@ -51,6 +56,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -64,19 +74,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
+RUN --mount=target=/var/cache/yum,type=cache,id=yum-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
microdnf install -y --nodocs --setopt=install_weak_deps=0 \
bzip2 \
bzip2-devel \
@@ -88,50 +103,63 @@ RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
libffi-devel \
libstdc++-devel \
libuuid-devel \
+ libxml2-devel \
mpdecimal \
+ ncurses-devel \
+ redhat-rpm-config \
sqlite \
sqlite-devel \
sqlite-libs \
xz-devel \
+ xz-libs \
zlib-devel \
&& curl -LsSf --retry 8 https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(rpm --eval '%{_target_cpu}')-redhat-linux" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(rpm --eval '%{optflags}')" \
+ && ldflags="$(rpm --eval '%{__global_ldflags}')" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-ubi9 b/src/docker/Dockerfile-ubi9
index 0cec0737..c407608d 100644
--- a/src/docker/Dockerfile-ubi9
+++ b/src/docker/Dockerfile-ubi9
@@ -1,4 +1,7 @@
-FROM registry.access.redhat.com/ubi9-minimal:9.4@sha256:104cf11d890aeb7dd5728b7d7732e175a0e4018f1bb00d2faebcc8f6bf29bd52 AS base
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar
+
+FROM registry.access.redhat.com/ubi9-minimal:9.4@sha256:f182b500ff167918ca1010595311cf162464f3aa1cab755383d38be61b4d30aa AS base
# Configure local user
ENV USER=root
@@ -9,16 +12,17 @@ ENV PYTHONDONTWRITEBYTECODE=1
# Install:
# - ASP.NET Core runtime
-# - Azure CLI system requirements (Python 3.11, C/Rust build tools for libs non pre-built on this platform)
+# - Azure CLI system requirements (C/Rust build tools for libs non pre-built on this platform)
# - Azure Pipelines agent system requirements
# - fuse-overlayfs, iptables, shadow-utils, for BuildKit
-# - gzip, make, tar, unzip, wget, zip, zstd for developer ease-of-life
+# - gzip, make, tar, unzip, wget, zip, zstd, dnsutils, rsync, for developer ease-of-life
# - zsh, for inter-operability
-RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
+RUN --mount=target=/var/cache/yum,type=cache,id=yum-${TARGETPLATFORM},sharing=locked \
microdnf install -y --nodocs --setopt=install_weak_deps=0 \
aspnetcore-runtime-8.0 \
ca-certificates \
cargo \
+ dnsutils \
findutils \
fuse-overlayfs \
gcc \
@@ -35,6 +39,7 @@ RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
openssl \
openssl-devel \
pkg-config \
+ rsync \
shadow-utils \
sudo \
tar \
@@ -50,6 +55,11 @@ COPY arch.sh .
RUN chmod +x arch.sh \
&& bash arch.sh
+# Persist Python version
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
+
FROM base AS rootlesskit
# Install Go, then verify installation
@@ -63,19 +73,24 @@ RUN go version
# Install RootlessKit, then verify installation
ARG ROOTLESSKIT_VERSION
ENV ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION}
-RUN git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
- && make --directory rootlesskit \
- && make --directory rootlesskit install \
+RUN --mount=target=/rootlesskit-${ROOTLESSKIT_VERSION},type=cache,id=rootlesskit-${ROOTLESSKIT_VERSION}-${TARGETPLATFORM},sharing=locked \
+ git clone --depth 1 --branch v${ROOTLESSKIT_VERSION} https://github.com/rootless-containers/rootlesskit.git rootlesskit \
+ # Ugly but that's work
+ && cp -r rootlesskit/* rootlesskit-${ROOTLESSKIT_VERSION} \
&& rm -rf rootlesskit \
+ && cd rootlesskit-${ROOTLESSKIT_VERSION} \
+ && make \
+ && make install \
+ && cd .. \
&& rootlesskit --version \
&& rootlessctl --version
FROM base AS python
-# Build Python 3.12 from source, then verify installation
+# Build Python from source, then verify installation
ARG PYTHON_VERSION
ENV PYTHON_VERSION=${PYTHON_VERSION}
-RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
+RUN --mount=target=/var/cache/yum,type=cache,id=yum-${TARGETPLATFORM},sharing=locked --mount=target=/Python-${PYTHON_VERSION},type=cache,id=python-${PYTHON_VERSION}-${TARGETPLATFORM},sharing=locked \
microdnf install -y --nodocs --setopt=install_weak_deps=0 \
bzip2 \
bzip2-devel \
@@ -87,50 +102,63 @@ RUN --mount=target=/var/cache/yum,type=cache,sharing=locked \
libffi-devel \
libstdc++-devel \
libuuid-devel \
+ libxml2-devel \
mpdecimal \
+ ncurses-devel \
+ redhat-rpm-config \
sqlite \
sqlite-devel \
sqlite-libs \
xz-devel \
+ xz-libs \
zlib-devel \
&& curl -LsSf --retry 8 --retry-all-errors https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -o python.tgz \
&& tar -xzf python.tgz \
+ && rm python.tgz \
&& cd Python-${PYTHON_VERSION} \
+ && gnu_arch="$(rpm --eval '%{_target_cpu}')-redhat-linux" \
&& ./configure \
+ --build=$gnu_arch \
--enable-optimizations \
--with-ensurepip=install \
--with-lto \
- && make -j$(nproc) \
+ && make profile-removal \
+ && extra_cflags="$(rpm --eval '%{optflags}')" \
+ && ldflags="$(rpm --eval '%{__global_ldflags}')" \
+ && make -j $(nproc) "EXTRA_CFLAGS=${extra_cflags:-}" "LDFLAGS=${ldflags:-}" \
&& make install \
- && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null \
+ && cd .. \
&& python3 --version \
- && python3 -m pip --version
+ && python3 -m pip --version \
+ && find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
FROM base
# Install Python, then verify installation
-COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
-COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
-RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \
- && ln -s /usr/local/bin/python3.12 /usr/local/bin/python \
+COPY --from=python /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR}
+COPY --from=python /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/lib/python${PYTHON_VERSION_MAJOR_MINOR}
+RUN ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python3 \
+ && ln -s /usr/local/bin/python${PYTHON_VERSION_MAJOR_MINOR} /usr/local/bin/python \
+ && python --version \
&& python3 --version \
+ && python${PYTHON_VERSION_MAJOR_MINOR} --version \
&& python3 -m pip --version
# Install Python build tools
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
- install \
+ install \
setuptools wheel \
&& find / -depth -type d -name __pycache__ -exec rm -rf {} \; 2> /dev/null
# Install Azure CLI, then verify installation
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
-RUN python3 -m pip \
+RUN --mount=target=/${USER}/.cache/pip,type=cache,id=pip-${PYTHON_VERSION_MAJOR_MINOR}-${TARGETPLATFORM},sharing=locked \
+ python3 -m pip \
--disable-pip-version-check \
- --no-cache-dir \
--quiet \
install \
azure-cli==${AZURE_CLI_VERSION} \
diff --git a/src/docker/Dockerfile-win-ltsc2019 b/src/docker/Dockerfile-win-ltsc2019
index 36301259..0f87878c 100644
--- a/src/docker/Dockerfile-win-ltsc2019
+++ b/src/docker/Dockerfile-win-ltsc2019
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar,WorkdirRelativePath
+
FROM mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2019@sha256:64ae795d03e5baff18006980118cc43089f4cf51c1f7ae84b41421772633bd01
# Configure local user
@@ -35,11 +38,16 @@ RUN mkdir 'C:\Program Files\jq' \
RUN jq --version
# Install Python, then verify installation
-ARG PYTHON_VERSION
-ENV PYTHON_VERSION=${PYTHON_VERSION}
+# TODO: Clean up "__pycache__" folders on disk
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
RUN curl -LsSf --retry 8 --retry-all-errors "https://python.org/ftp/python/${Env:PYTHON_VERSION}/python-${Env:PYTHON_VERSION}-amd64.exe" -o python.exe \
&& Start-Process python.exe -Wait -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1 Include_test=0' \
&& Remove-Item python.exe
+
+# Install Python build tools
+# TODO: Clean up "__pycache__" folders on disk
RUN python --version \
&& python -m pip \
--disable-pip-version-check \
@@ -49,6 +57,7 @@ RUN python --version \
setuptools wheel
# Install Azure CLI, then verify installation
+# TODO: Clean up "__pycache__" folders on disk
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
RUN python -m pip \
diff --git a/src/docker/Dockerfile-win-ltsc2022 b/src/docker/Dockerfile-win-ltsc2022
index 06bebb2c..ebc86c80 100644
--- a/src/docker/Dockerfile-win-ltsc2022
+++ b/src/docker/Dockerfile-win-ltsc2022
@@ -1,3 +1,6 @@
+# syntax=docker/dockerfile:1
+# check=skip=UndefinedVar,WorkdirRelativePath
+
FROM mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2022@sha256:a5daa91a8bbf6dbbdfde03490343c3234b356dcbb0740cd33fd1417b34670c38
# Configure local user
@@ -35,11 +38,16 @@ RUN mkdir 'C:\Program Files\jq' \
RUN jq --version
# Install Python, then verify installation
-ARG PYTHON_VERSION
-ENV PYTHON_VERSION=${PYTHON_VERSION}
+# TODO: Clean up "__pycache__" folders on disk
+ARG PYTHON_VERSION_MAJOR_MINOR
+ARG PYTHON_VERSION_PATCH
+ENV PYTHON_VERSION=${PYTHON_VERSION_MAJOR_MINOR}.${PYTHON_VERSION_PATCH}
RUN curl -LsSf --retry 8 --retry-all-errors "https://python.org/ftp/python/${Env:PYTHON_VERSION}/python-${Env:PYTHON_VERSION}-amd64.exe" -o python.exe \
&& Start-Process python.exe -Wait -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1 Include_test=0' \
&& Remove-Item python.exe
+
+# Install Python build tools
+# TODO: Clean up "__pycache__" folders on disk
RUN python --version \
&& python -m pip \
--disable-pip-version-check \
@@ -49,6 +57,7 @@ RUN python --version \
setuptools wheel
# Install Azure CLI, then verify installation
+# TODO: Clean up "__pycache__" folders on disk
ARG AZURE_CLI_VERSION
ENV AZURE_CLI_VERSION=${AZURE_CLI_VERSION}
RUN python -m pip \
diff --git a/src/docker/start.ps1 b/src/docker/start.ps1
index 8527beb1..9ba94486 100644
--- a/src/docker/start.ps1
+++ b/src/docker/start.ps1
@@ -1,42 +1,78 @@
-$AZP_AGENT_NAME = $Env:AZP_AGENT_NAME
-$AZP_CUSTOM_CERT_PEM = $Env:AZP_CUSTOM_CERT_PEM
-$AZP_POOL = $Env:AZP_POOL
-$AZP_TOKEN = $Env:AZP_TOKEN
-$AZP_URL = $Env:AZP_URL
-$AZP_WORK = $Env:AZP_WORK
-
-if ($null -eq $AZP_URL -or $AZP_URL -eq "") {
- throw "error: missing AZP_URL environment variable"
+# Start the Azure DevOps agent in a Windows container
+#
+# Agent is always registered. It is removed from the server only when the agent is not a template job. After 60 secs, it tries to shut down the agent gracefully, waiting for the current job to finish, if any.
+#
+# Environment variables:
+# - AZP_AGENT_NAME: Agent name (default: hostname)
+# - AZP_CUSTOM_CERT_PEM: Custom SSL certificates directory (default: empty)
+# - AZP_POOL: Agent pool name
+# - AZP_TEMPLATE_JOB: Template job flag (default: 0)
+# - AZP_TOKEN: Personal access token
+# - AZP_URL: Server URL
+# - AZP_WORK: Work directory
+
+##
+# Misc functions
+##
+
+function Write-Header() {
+ Write-Host "➡️ $1" -ForegroundColor Cyan
+}
+
+function Write-Warning() {
+ Write-Host "⚠️ $1" -ForegroundColor Yellow
}
-if ($null -eq $AZP_TOKEN -or $AZP_TOKEN -eq "") {
- throw "error: missing AZP_TOKEN environment variable"
+function Raise-Error() {
+ throw "❌ $1"
}
-if ($null -eq $AZP_POOL -or $AZP_POOL -eq "") {
- throw "error: missing AZP_POOL environment variable"
+##
+# Argument parsing
+##
+
+if ($null -eq $Env:AZP_URL -or $Env:AZP_URL -eq "") {
+ Raise-Error "Missing AZP_URL environment variable"
}
-# If AZP_AGENT_NAME is not set, use the container hostname
-if ($null -eq $AZP_AGENT_NAME -or $AZP_AGENT_NAME -eq "") {
- Write-Host "warn: missing AZP_AGENT_NAME environment variable"
- $AZP_AGENT_NAME = $Env:COMPUTERNAME
+if ($null -eq $Env:AZP_TOKEN -or $Env:AZP_TOKEN -eq "") {
+ Raise-Error "Missing AZP_TOKEN environment variable"
}
-if ($null -eq $AZP_WORK -or $AZP_WORK -eq "") {
- throw "error: missing AZP_WORK environment variable"
+if ($null -eq $Env:AZP_POOL -or $Env:AZP_POOL -eq "") {
+ Raise-Error "Missing AZP_POOL environment variable"
}
-if (!(Test-Path $AZP_WORK)) {
- throw "error: work dir AZP_WORK ($AZP_WORK) is not writeable or does not exist"
+# If name is not set, use the hostname
+if ($null -eq $Env:AZP_AGENT_NAME -or $Env:AZP_AGENT_NAME -eq "") {
+ Write-Warning "Missing AZP_AGENT_NAME environment variable, using hostname"
+ $Env:AZP_AGENT_NAME = $Env:COMPUTERNAME
}
-function Write-Header() {
- Write-Host "> $1" -ForegroundColor Cyan
+if ($null -eq $Env:AZP_WORK -or $Env:AZP_WORK -eq "") {
+ Raise-Error "Missing AZP_WORK environment variable"
}
+if (!(Test-Path $Env:AZP_WORK)) {
+ Write-Warning "Work dir AZP_WORK ($Env:AZP_WORK) does not exist, creating it, but reliability is not guaranteed"
+ New-Item -Path $Env:AZP_WORK -ItemType Directory
+}
+
+$isTemplateJob = $false
+if ($Env:AZP_TEMPLATE_JOB -eq "1") {
+ Write-Warning "Template job enabled, agent cannot be used for running jobs"
+ $isTemplateJob = $true
+ $Env:AZP_AGENT_NAME = "$Env:AZP_AGENT_NAME-template"
+}
+
+Write-Header "Running agent $Env:AZP_AGENT_NAME in pool $Env:AZP_POOL"
+
+##
+# Cleanup function
+##
+
function Unregister {
- Write-Host "Unregister, removing agent from server"
+ Write-Header "Removing agent"
# If the agent has some running jobs, the configuration removal process will fail; so, give it some time to finish the job
while ($true) {
@@ -44,21 +80,26 @@ function Unregister {
# If the agent is removed successfully, exit the loop
& config.cmd remove `
--auth PAT `
- --token $AZP_TOKEN `
+ --token $Env:AZP_TOKEN `
--unattended
break
} catch {
- Write-Host "Retrying in 15 secs"
+ Write-Host "A job is still running, waiting 15 seconds before retrying the removal"
Start-Sleep -Seconds 15
}
}
}
-if ((Test-Path $AZP_CUSTOM_CERT_PEM) -and ((Get-ChildItem $AZP_CUSTOM_CERT_PEM).Count -gt 0)) {
- Write-Header "Adding custom SSL certificates"
- Write-Host "Searching for *.crt in $AZP_CUSTOM_CERT_PEM"
+##
+# Custom SSL certificates
+##
+
+Write-Header "Adding custom SSL certificates"
+
+if ((Test-Path $Env:AZP_CUSTOM_CERT_PEM) -and ((Get-ChildItem $Env:AZP_CUSTOM_CERT_PEM).Count -gt 0)) {
+ Write-Host "Searching for *.crt in $Env:AZP_CUSTOM_CERT_PEM"
- Get-ChildItem $AZP_CUSTOM_CERT_PEM -Filter *.crt | ForEach-Object {
+ Get-ChildItem $Env:AZP_CUSTOM_CERT_PEM -Filter *.crt | ForEach-Object {
Write-Host "Certificate $($_.Name)"
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($_.FullName)
@@ -68,36 +109,62 @@ if ((Test-Path $AZP_CUSTOM_CERT_PEM) -and ((Get-ChildItem $AZP_CUSTOM_CERT_PEM).
Write-Host "Updating certificates keychain"
Import-Certificate -FilePath $_.FullName -CertStoreLocation Cert:\LocalMachine\Root
}
-
} else {
- Write-Header "No custom SSL certificate provided"
+ Write-Host "No custom SSL certificate provided"
}
+##
+# Agent configuration
+##
+
Write-Header "Configuring agent"
Set-Location $(Split-Path -Parent $MyInvocation.MyCommand.Definition)
& config.cmd `
--acceptTeeEula `
- --agent $AZP_AGENT_NAME `
+ --agent $Env:AZP_AGENT_NAME `
--auth PAT `
- --pool $AZP_POOL `
+ --pool $Env:AZP_POOL `
--replace `
- --token $AZP_TOKEN `
+ --token $Env:AZP_TOKEN `
--unattended `
- --url $AZP_URL `
- --work $AZP_WORK
+ --url $Env:AZP_URL `
+ --work $Env:AZP_WORK
+
+##
+# Agent execution
+##
Write-Header "Running agent"
-# Unregister on success, Ctrl+C, and SIGTERM
-try {
- # Running it with the --once flag at the end will shut down the agent after the build is executed
- & run.cmd $Args --once
-} finally {
- Unregister
+# Running it with the --once flag at the end will shut down the agent after the build is executed
+if ($isTemplateJob) {
+ Write-Host "Agent will be stopped after 1 min"
+ # Run the agent for a minute
+ Start-Job -ScriptBlock {
+ Start-Sleep -Seconds 60
+ & run.cmd $Args --once
+ }
+} else {
+ try {
+ # Run the countdown for fast-clean if no job is using the agent after a delay
+ Start-Job -ScriptBlock {
+ Start-Sleep -Seconds 60
+ Unregister
+ }
+ # Run the agent
+ & run.cmd $Args --once
+ } finally {
+ # Unregister on success, Ctrl+C, and SIGTERM
+ Unregister
+ }
}
+##
+# Diagnostics
+##
+
Write-Header "Printing agent diag logs"
Get-Content $AGENT_DIAGLOGPATH/*.log
diff --git a/src/docker/start.sh b/src/docker/start.sh
index 145e97b6..e59d9e84 100644
--- a/src/docker/start.sh
+++ b/src/docker/start.sh
@@ -1,45 +1,85 @@
#!/bin/bash
set -e
+# Start the Azure DevOps agent in a Windows container
+#
+# Agent is always registered. It is removed from the server only when the agent is not a template job. After 60 secs, it tries to shut down the agent gracefully, waiting for the current job to finish, if any.
+#
+# Environment variables:
+# - AZP_AGENT_NAME: Agent name (default: hostname)
+# - AZP_CUSTOM_CERT_PEM: Custom SSL certificates directory (default: empty)
+# - AZP_POOL: Agent pool name
+# - AZP_TEMPLATE_JOB: Template job flag (default: 0)
+# - AZP_TOKEN: Personal access token
+# - AZP_URL: Server URL
+# - AZP_WORK: Work directory
+
+##
+# Misc functions
+##
+
+write_header() {
+ lightcyan='\033[1;36m'
+ nocolor='\033[0m'
+ echo -e "${lightcyan}➡️ $1${nocolor}"
+}
+
+write_warning() {
+ yellow='\033[1;33m'
+ nocolor='\033[0m'
+ echo -e "${yellow}⚠️ $1${nocolor}"
+}
+
+raise_error() {
+ red='\033[1;31m'
+ nocolor='\033[0m'
+ echo 1>&2 -e "${red}❌ $1${nocolor}"
+}
+
+##
+# Argument parsing
+##
+
if [ -z "$AZP_URL" ]; then
- echo 1>&2 "error: missing AZP_URL environment variable"
+ raise_error "Missing AZP_URL environment variable"
exit 1
fi
if [ -z "$AZP_TOKEN" ]; then
- echo 1>&2 "error: missing AZP_TOKEN environment variable"
+ raise_error "Missing AZP_TOKEN environment variable"
exit 1
fi
if [ -z "$AZP_POOL" ]; then
- echo 1>&2 "error: missing AZP_POOL environment variable"
+ raise_error "Missing AZP_POOL environment variable"
exit 1
fi
-# If AZP_AGENT_NAME is not set, use the container hostname
+# If name is not set, use the hostname
if [ -z "$AZP_AGENT_NAME" ]; then
- echo "warn: missing AZP_AGENT_NAME environment variable"
+ write_warning "Missing AZP_AGENT_NAME environment variable, using hostname"
AZP_AGENT_NAME=$(hostname)
fi
-if [ -z "$AZP_AGENT_NAME" ]; then
- echo 1>&2 "error: missing AZP_AGENT_NAME environment variable"
- exit 1
+if [ ! -w "$AZP_WORK" ]; then
+ write_warning "Work dir AZP_WORK (${AZP_WORK}) does not exist, creating it, but reliability is not guaranteed"
+ mkdir -p "$AZP_WORK"
fi
-if [ ! -w "$AZP_WORK" ]; then
- echo 1>&2 "error: work dir AZP_WORK (${AZP_WORK}) is not writeable or does not exist"
- exit 1
+if [ "$AZP_TEMPLATE_JOB" == "1" ]; then
+ write_warning "Template job enabled, agent cannot be used for running jobs"
+ is_template_job="true"
+ AZP_AGENT_NAME="${AZP_AGENT_NAME}-template"
fi
-write_header() {
- lightcyan='\033[1;36m'
- nocolor='\033[0m'
- echo -e "${lightcyan}➡️ $1${nocolor}"
-}
+write_header "Running agent $AZP_AGENT_NAME in pool $AZP_POOL"
+
+##
+# Cleanup function
+##
unregister() {
- write_header "Unregister, removing agent from server"
+ write_header "Removing agent"
# If the agent has some running jobs, the configuration removal process will fail ; so, give it some time to finish the job
while true; do
@@ -50,27 +90,32 @@ unregister() {
--unattended \
&& break
- echo "Retrying in 15 secs"
+ echo "A job is still running, waiting 15 seconds before retrying the removal"
sleep 15
done
}
+##
+# Custom SSL certificates
+##
+
+write_header "Adding custom SSL certificates"
+
if [ -d "$AZP_CUSTOM_CERT_PEM" ] && [ "$(ls -A $AZP_CUSTOM_CERT_PEM)" ]; then
- write_header "Adding custom SSL certificates"
echo "Searching for *.crt in $AZP_CUSTOM_CERT_PEM"
# Debian-based systems
if [ -s /etc/debian_version ]; then
- certPath="/usr/local/share/ca-certificates"
- mkdir -p $certPath
+ cert_path="/usr/local/share/ca-certificates"
+ mkdir -p $cert_path
# Copy certificates to the certificate path
- cp $AZP_CUSTOM_CERT_PEM/*.crt $certPath
+ cp $AZP_CUSTOM_CERT_PEM/*.crt $cert_path
# Display certificates information
- for certFile in $AZP_CUSTOM_CERT_PEM/*.crt; do
- echo "Certificate $(basename $certFile)"
- openssl x509 -inform PEM -in $certFile -noout -issuer -subject -dates
+ for cert_file in $AZP_CUSTOM_CERT_PEM/*.crt; do
+ echo "Certificate $(basename $cert_file)"
+ openssl x509 -inform PEM -in $cert_file -noout -issuer -subject -dates
done
echo "Updating certificates keychain"
@@ -79,25 +124,29 @@ if [ -d "$AZP_CUSTOM_CERT_PEM" ] && [ "$(ls -A $AZP_CUSTOM_CERT_PEM)" ]; then
# RHEL-based systems
if [ -s /etc/redhat-release ]; then
- certPath="/etc/ca-certificates/trust-source/anchors"
- mkdir -p $certPath
+ cert_path="/etc/ca-certificates/trust-source/anchors"
+ mkdir -p $cert_path
# Copy certificates to the certificate path
- cp $AZP_CUSTOM_CERT_PEM/*.crt $certPath
+ cp $AZP_CUSTOM_CERT_PEM/*.crt $cert_path
# Display certificates information
- for certFile in $AZP_CUSTOM_CERT_PEM/*.crt; do
- echo "Certificate $(basename $certFile)"
- openssl x509 -inform PEM -in $certFile -noout -issuer -subject -dates
+ for cert_file in $AZP_CUSTOM_CERT_PEM/*.crt; do
+ echo "Certificate $(basename $cert_file)"
+ openssl x509 -inform PEM -in $cert_file -noout -issuer -subject -dates
done
echo "Updating certificates keychain"
update-ca-trust extract
fi
else
- write_header "No custom SSL certificate provided"
+ echo "No custom SSL certificate provided"
fi
+##
+# Agent configuration
+##
+
write_header "Configuring agent"
cd $(dirname "$0")
@@ -117,22 +166,38 @@ bash config.sh \
# See: https://stackoverflow.com/a/62183992/12732154
wait $!
-# Unregister on success
-trap 'unregister; exit 0' EXIT
-# Unregister on Ctrl+C
-trap 'unregister; exit 130' INT
-# Unregister on SIGTERM
-trap 'unregister; exit 143' TERM
+##
+# Agent execution
+##
write_header "Running agent"
# Running it with the --once flag at the end will shut down the agent after the build is executed
-bash run-docker.sh "$@" --once &
+if [ "$is_template_job" == "true" ]; then
+ echo "Agent will be stopped after 1 min"
+ # Run the agent for a minute
+ timeout --preserve-status 1m bash run-docker.sh "$@" --once &
+else
+ # Unregister on success
+ trap 'unregister; exit 0' EXIT
+ # Unregister on Ctrl+C
+ trap 'unregister; exit 130' INT
+ # Unregister on SIGTERM
+ trap 'unregister; exit 143' TERM
+ # Run the countdown for fast-clean if no job is using the agent after a delay
+ sleep 60 && unregister &
+ # Run the agent
+ bash run-docker.sh "$@" --once &
+fi
# Fake the exit code of the agent for the prevent Kubernetes to detect the pod as failed (this is intended)
# See: https://stackoverflow.com/a/62183992/12732154
wait $!
+##
+# Diagnostics
+##
+
write_header "Printing agent diag logs"
cat $AGENT_DIAGLOGPATH/*.log
diff --git a/src/helm/blue-agent/templates/_helpers.tpl b/src/helm/blue-agent/templates/_helpers.tpl
index 046fd9c2..98deb997 100644
--- a/src/helm/blue-agent/templates/_helpers.tpl
+++ b/src/helm/blue-agent/templates/_helpers.tpl
@@ -103,6 +103,12 @@ windowsOptions:
allowPrivilegeEscalation: false
runAsUser: 0
capabilities:
+ # Add enough default capabilities to allow the agent to unzip files and change file ownership
+ # See: https://github.com/clemlesne/blue-agent/issues/23#issuecomment-2444929885
+ add:
+ - CHOWN
+ - FOWNER
+ # Remove all default root capabilities to ensure the container is running with the least privileges
drop: ["ALL"]
{{- end }}
{{- end }}
@@ -113,8 +119,9 @@ Common definition for Pod object.
Usage example:
{{- $data := dict
+ "azpAgentName" (dict "value" (include "blue-agent.fullname" .))
+ "isTemplateJob" "1"
"restartPolicy" "Always"
- "azpAgentName" (dict "value" (printf "%s-%s" (include "blue-agent.fullname" .) "template"))
}}
{{- include "blue-agent.podSharedTemplate" (merge (dict "Args" $data) . ) | nindent 6 }}
*/}}
@@ -200,6 +207,8 @@ containers:
secretKeyRef:
name: {{ include "blue-agent.secretName" . }}
key: personalAccessToken
+ - name: AZP_TEMPLATE_JOB
+ value: {{ .Args.isTemplateJob }}
# Agent capabilities
- name: flavor_{{ .Values.image.flavor | required "A value for .Values.image.flavor is required" }}
- name: version_{{ default .Chart.Version .Values.image.version }}
diff --git a/src/helm/blue-agent/templates/deployment.yaml b/src/helm/blue-agent/templates/deployment.yaml
index c86018da..b44196cb 100644
--- a/src/helm/blue-agent/templates/deployment.yaml
+++ b/src/helm/blue-agent/templates/deployment.yaml
@@ -28,8 +28,9 @@ spec:
{{- end }}
spec:
{{- $data := dict
- "restartPolicy" "Always"
"azpAgentName" (dict "valueFrom" (dict "fieldRef" (dict "apiVersion" "v1" "fieldPath" "metadata.name" )))
+ "isTemplateJob" "1"
+ "restartPolicy" "Always"
}}
{{- include "blue-agent.podSharedTemplate" (merge (dict "Args" $data) . ) | nindent 6 }}
{{- end }}
diff --git a/src/helm/blue-agent/templates/hpa.yaml b/src/helm/blue-agent/templates/hpa.yaml
index 92a74d7a..b30227cd 100644
--- a/src/helm/blue-agent/templates/hpa.yaml
+++ b/src/helm/blue-agent/templates/hpa.yaml
@@ -24,7 +24,7 @@ spec:
failedJobsHistoryLimit: {{ .Values.pipelines.cleanup.failed | int | required "A value for .Values.pipelines.cleanup.failed is required" }}
maxReplicaCount: {{ .Values.autoscaling.maxReplicas | int | required "A value for .Values.autoscaling.maxReplicas is required" }}
minReplicaCount: {{ .Values.autoscaling.minReplicas | int | required "A value for .Values.autoscaling.minReplicas is required" }}
- pollingInterval: 15
+ pollingInterval: {{ .Values.autoscaling.pollingInterval | int | required "A value for .Values.autoscaling.pollingInterval is required" }}
successfulJobsHistoryLimit: {{ .Values.pipelines.cleanup.successful | int | required "A value for .Values.pipelines.cleanup.successful is required" }}
jobTargetRef:
activeDeadlineSeconds: {{ .Values.pipelines.timeout | int | required "A value for .Values.pipelines.timeout is required" }}
@@ -43,8 +43,9 @@ spec:
{{- end }}
spec:
{{- $data := dict
- "restartPolicy" "Never"
"azpAgentName" (dict "valueFrom" (dict "fieldRef" (dict "apiVersion" "v1" "fieldPath" "metadata.name" )))
+ "isTemplateJob" "0"
+ "restartPolicy" "Never"
}}
{{- include "blue-agent.podSharedTemplate" (merge (dict "Args" $data) . ) | nindent 8 }}
rollout:
diff --git a/src/helm/blue-agent/templates/pod.yaml b/src/helm/blue-agent/templates/pod.yaml
index 0f012f74..8f96e958 100644
--- a/src/helm/blue-agent/templates/pod.yaml
+++ b/src/helm/blue-agent/templates/pod.yaml
@@ -13,8 +13,9 @@ metadata:
{{- end }}
spec:
{{- $data := dict
+ "azpAgentName" (dict "value" (include "blue-agent.fullname" .))
+ "isTemplateJob" "1"
"restartPolicy" "Never"
- "azpAgentName" (dict "value" (printf "%s-%s" (include "blue-agent.fullname" .) "template"))
}}
{{- include "blue-agent.podSharedTemplate" (merge (dict "Args" $data) . ) | nindent 2 }}
{{- end }}
diff --git a/src/helm/blue-agent/values.yaml b/src/helm/blue-agent/values.yaml
index 86939f8e..26ecbb61 100644
--- a/src/helm/blue-agent/values.yaml
+++ b/src/helm/blue-agent/values.yaml
@@ -29,6 +29,9 @@ autoscaling:
minReplicas: 0
# Maximum number of replicas, default is 100 to prevent misconfiguration
maxReplicas: 100
+ # Interval in seconds to poll for new jobs
+ # Warning: A low value will cause rate limiting or throttling, and can cause high load on the Azure DevOps API
+ pollingInterval: 10
# Pipeline configuration
pipelines:
diff --git a/test/azure-devops/exists.sh b/test/azure-devops/exists.sh
deleted file mode 100644
index 9ec3ba7f..00000000
--- a/test/azure-devops/exists.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-###
-# Test the existence of an Azure DevOps agent in a pool.
-#
-# If the agent is found, the script will exit with status 0. Will retry every 5 seconds, indefinitely, until the agent is found.
-#
-# Usage: ./exists.sh
-###
-
-#!/bin/bash
-set -e
-
-agent="$1"
-
-if [ -z "$agent" ]; then
- echo "Test the existence of an Azure DevOps agent in a pool."
- echo "Usage: $1 "
- exit 1
-fi
-
-pool_name="github-actions"
-
-echo "Testing existence of agent ${agent} in pool ${pool_name}"
-
-# Get the pool id
-pool_id=$(az pipelines pool list \
- --pool-name "${pool_name}" \
- --query "[0].id")
-
-if [ -z "$pool_id" ]; then
- echo "Pool ${pool_name} not found"
- exit 1
-fi
-
-while true; do
- agent_json=$(az pipelines agent list \
- --pool-id "${pool_id}" \
- | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"${agent}\")) and .status == \"online\"))")
- if [ -n "$agent_json" ] && [ "$agent_json" != "null" ]; then
- break
- fi
- echo "Agent ${agent} not found in pool ${pool_name} (${pool_id}), retrying in 5 seconds"
- sleep 5
-done
-
-agent_name=$(echo "${agent_json}" | jq -r ".name")
-agent_id=$(echo "${agent_json}" | jq -r ".id")
-
-echo "✅ Agent ${agent_name} (${agent_id}) found in pool ${pool_name} (${pool_id})"
-
-agent_capabilities=$(az pipelines agent show \
- --agent-id "${agent_id}" \
- --include-capabilities \
- --pool-id "${pool_id}" \
- | jq -r ".systemCapabilities")
-
-echo "Capabilities:"
-echo ${agent_capabilities} | jq -r "to_entries | map(\"\(.key)=\(.value | tostring)\") | sort[]"
diff --git a/test/azure-devops/has-been-cleaned.sh b/test/azure-devops/has-been-cleaned.sh
deleted file mode 100644
index b6e66531..00000000
--- a/test/azure-devops/has-been-cleaned.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-set -e
-
-agent="$1"
-
-if [ -z "$agent" ]; then
- echo "Test is an Azure DevOps has been cleaned up from a pool."
- echo "Usage: $1 "
- exit 1
-fi
-
-pool_name="github-actions"
-
-echo "Testing existence of agent ${agent} in pool ${pool_name}"
-
-# Get the pool id
-pool_id=$(az pipelines pool list \
- --pool-name "${pool_name}" \
- --query "[0].id")
-
-if [ -z "$pool_id" ]; then
- echo "Pool ${pool_name} not found"
- exit 1
-fi
-
-# TODO: Add a discriminator to the agent properties, like an environment variable, to ensure there is no test collision when running multiple tests in parallel from the same branch.
-# Wait for the agent ot be removed, as it is cleaned up asynchronously
-for i in {1..12}; do
- agent_name=$(az pipelines agent list \
- --pool-id "${pool_id}" \
- | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"${agent}\")) and .status == \"offline\")).name")
- if [ -n "$agent_name" ] && [ "$agent_name" != "null" ]; then
- echo "Agent ${agent_name} exists, retrying in 5 seconds"
- sleep 5
- else
- echo "✅ Agent ${agent} has been cleaned from pool ${pool_name} (${pool_id})"
- exit 0
- fi
-done
-
-echo "❌ Agent ${agent} has not been cleaned from pool ${pool_name} (${pool_id})"
-exit 1
diff --git a/test/azure-devops/job-timeout.sh b/test/azure-devops/job-timeout.sh
new file mode 100644
index 00000000..7091ec15
--- /dev/null
+++ b/test/azure-devops/job-timeout.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -e
+
+agent="$1"
+pool_id="$2"
+rg_name="$3"
+
+if [ -z "$agent" ] || [ -z "$pool_id" ] || [ -z "$rg_name" ]; then
+ echo "Test is the agent properly timed out if it has no activity."
+ echo "Usage: $1 $2 $3 "
+ exit 1
+fi
+
+echo "➡️ Testing timeout of agent $agent in pool $pool_id, from job $agent in resource group $rg_name"
+
+# Trigger the job
+echo "Triggering job"
+az containerapp job start \
+ --name $agent \
+ --resource-group $rg_name
+
+# Wait for the agent to start
+echo "⏳ Waiting for the agent to start"
+while true; do
+ agent_id=$(az pipelines agent list \
+ --pool-id $pool_id \
+ | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"$agent\")) and .status == \"online\")).id // empty")
+ if [ -n "$agent_id" ]; then
+ break
+ fi
+ echo "Not found, retrying in 5 seconds"
+ sleep 5
+done
+
+# Default is 1 minute, but we offer some extra time
+echo "⏳ Waiting 2 minutes for agent to time out"
+sleep 120
+
+# Check the agent is offline
+agent_status=$(az pipelines agent show \
+ --agent-id $agent_id \
+ --pool-id $pool_id \
+ | jq -r ".status")
+echo "Agent status: $agent_status"
+
+# Fail if the agent is still online
+if [ "$agent_status" == "offline" ]; then
+ echo "❌ Agent did not time out"
+ exit 1
+fi
+
+echo "✅ Agent properly timed out"
diff --git a/test/azure-devops/pipeline.sh b/test/azure-devops/pipeline.sh
index 9a79e44f..98366e2f 100644
--- a/test/azure-devops/pipeline.sh
+++ b/test/azure-devops/pipeline.sh
@@ -12,84 +12,95 @@ if [ -z "$prefix" ] || [ -z "$pipeline" ] || [ -z "$flavor" ] || [ -z "$version"
exit 1
fi
-pipeline_path="test/pipeline/${pipeline}.yaml"
-if [ ! -f "${PWD}/${pipeline_path}" ]; then
- echo "Pipeline ${pipeline_path} does not exist"
+echo "➡️ Running pipeline $pipeline for flavor $flavor and version $version"
+
+pipeline_path="test/pipeline/$pipeline.yaml"
+if [ ! -f "$PWD/$pipeline_path" ]; then
+ echo "Pipeline $pipeline_path does not exist"
echo "Available pipelines:"
- ls -1 "${PWD}/test/pipeline/*.yaml" | sed 's/\.yaml$//'
+ ls -1 "$PWD/test/pipeline/*.yaml" | sed 's/\.yaml$//'
exit 1
fi
organization_url=$(az devops configure --list | grep 'organization =' | cut -d'=' -f2 | tr -d '[:space:]')
pool_name="github-actions"
-project_name="${prefix}-${flavor}"
-service_connection_name="${project_name}"
-pipeline_name="${pipeline}"
+project_name="$prefix-$flavor"
+service_connection_name="$project_name"
+pipeline_name="$pipeline"
-echo "Creating project ${project_name} in organization ${organization_url}"
-if az devops project show --project "${project_name}" \
+# Create the project, service connection and pipeline
+echo "Creating project $project_name in organization $organization_url"
+if az devops project show --project "$project_name" \
&> /dev/null; then
- echo "Project ${project_name} already exists"
+ echo "Project $project_name already exists"
else
az devops project create \
- --description "Integration test for image `${flavor}`. Related to the project [blue-agent](https://github.com/clemlesne/blue-agent)." \
- --name "${project_name}" \
+ --description "Integration test for image `$flavor`. Related to the project [blue-agent](https://github.com/clemlesne/blue-agent)." \
+ --name "$project_name" \
--visibility public
fi
-project_id=$(az devops project show --project "${project_name}" \
+project_id=$(az devops project show --project "$project_name" \
| jq -r '.id')
-flock $HOME/.azure/azuredevops/config --command "az devops configure --defaults project=${project_id}"
+flock $HOME/.azure/azuredevops/config --command "az devops configure --defaults project=$project_id"
-echo "Getting agent pool ${pool_name}"
+# Get the agent pool
+echo "Getting agent pool $pool_name"
queue_id=$(az pipelines queue list \
- --query "[?name=='${pool_name}'].id" \
+ --query "[?name=='$pool_name'].id" \
| jq -r '.[0]')
-if [ -z "${queue_id}" ]; then
- echo "Agent pool ${pool_name} does not exist"
+if [ -z "$queue_id" ]; then
+ echo "Agent pool $pool_name does not exist"
exit 1
fi
+echo "Agent pool id: $queue_id"
-echo "Creating service connection ${service_connection_name}"
+# Create the service connection
+echo "Creating service connection $service_connection_name"
if az devops service-endpoint show \
--id $(az devops service-endpoint list \
- --query "[?name=='${service_connection_name}']" \
+ --query "[?name=='$service_connection_name']" \
| jq -r '.[0].id') \
&> /dev/null; then
- echo "Service connection ${service_connection_name} already exists"
+ echo "Service connection $service_connection_name already exists"
else
az devops service-endpoint github create \
- --name "${service_connection_name}" \
+ --name "$service_connection_name" \
--github-url $(git remote get-url origin)
fi
-service_connection_id=$(az devops service-endpoint list --query "[?name=='${service_connection_name}']" \
+service_connection_id=$(az devops service-endpoint list --query "[?name=='$service_connection_name']" \
| jq -r '.[0].id')
+echo "Service connection id: $service_connection_id"
-echo "Creating pipeline ${pipeline_name} in project ${project_name}"
-if az pipelines show --name "${pipeline_name}" \
+# Create the pipeline
+echo "Creating pipeline $pipeline_name in project $project_name"
+if az pipelines show --name "$pipeline_name" \
&> /dev/null; then
- echo "Pipeline ${pipeline_name} already exists"
+ echo "Pipeline $pipeline_name already exists"
else
az pipelines create \
--branch $(git rev-parse --abbrev-ref HEAD) \
- --description "Test pipeline `${pipeline}`. Created from GitHub Actions." \
- --name "${pipeline_name}" \
+ --description "Test pipeline `$pipeline`. Created from GitHub Actions." \
+ --name "$pipeline_name" \
+ --only-show-errors \
--repository $(git remote get-url origin) \
--repository-type github \
- --service-connection "${service_connection_id}" \
+ --service-connection "$service_connection_id" \
--skip-first-run \
- --yml-path "${pipeline_path}"
+ --yml-path "$pipeline_path"
fi
-pipeline_id=$(az pipelines show --name "${pipeline_name}" \
+pipeline_id=$(az pipelines show --name "$pipeline_name" \
| jq -r '.id')
+echo "Pipeline id: $pipeline_id"
-echo "Authorizing pipeline ${pipeline_name} to run on agent pool ${pool_name}"
+# Authorize the pipeline to run on the agent pool
+echo "Authorizing pipeline $pipeline_name to run on agent pool $pool_name"
# TODO: Use Azure CLI to auhorize the pipeline to run on the agent pool (see: https://github.com/Azure/azure-cli/issues/28111)
tmp_file=$(mktemp -t XXXXXX.json)
-cat < "${tmp_file}"
+cat < "$tmp_file"
{
"pipelines": [{
"authorized": true,
- "id": "${pipeline_id}"
+ "id": "$pipeline_id"
}]
}
EOF
@@ -97,52 +108,54 @@ az devops invoke \
--api-version 7.1-preview \
--area pipelinePermissions \
--http-method PATCH \
- --in-file "${tmp_file}" \
+ --in-file "$tmp_file" \
--resource pipelinePermissions \
- --route-parameters project=${project_id} resourceType=queue resourceId=${queue_id} \
+ --route-parameters project=$project_id resourceType=queue resourceId=$queue_id \
> /dev/null
-rm -f "${tmp_file}"
+rm -f "$tmp_file"
-echo "Running pipeline ${pipeline_name}"
+# Run the pipeline
+echo "Running pipeline $pipeline_name"
run_json=$(az pipelines run \
--commit-id $(git rev-parse HEAD) \
- --id "${pipeline_id}" \
- --parameters flavor="${flavor}" version="${version}")
-run_id=$(echo "${run_json}" | jq -r '.id')
-
-echo "Waiting for pipeline run ${run_id} to complete"
-echo "🔗 ${organization_url}/${project_name}/_build/results?buildId=${run_id}"
-
+ --id "$pipeline_id" \
+ --parameters flavor="$flavor" version="$version")
+run_id=$(echo "$run_json" | jq -r '.id')
+echo "Pipeline run id: $run_id"
+
+# Wait for the pipeline run to complete
+echo "⏳ Waiting for the pipeline run to complete"
+echo "🔗 $organization_url/$project_name/_build/results?buildId=$run_id"
timeout_seconds=900 # 15 minutes
start_time=$(date +%s)
while true; do
- run_json=$(az pipelines runs show --id "${run_id}")
- status=$(echo ${run_json} | jq -r '.status')
+ run_json=$(az pipelines runs show --id "$run_id")
+ status=$(echo $run_json | jq -r '.status')
- if [ "${status}" == "completed" ]; then
- result=$(echo "${run_json}" | jq -r '.result')
- validation_results=$(echo "${run_json}" | jq -r '.validationResults')
+ if [ "$status" == "completed" ]; then
+ result=$(echo "$run_json" | jq -r '.result')
+ validation_results=$(echo "$run_json" | jq -r '.validationResults')
echo "Validation results:"
- echo "${validation_results}" | jq
+ echo "$validation_results" | jq
- if [ "${result}" == "succeeded" ]; then
- echo "✅ Pipeline run ${run_id} succeeded"
+ if [ "$result" == "succeeded" ]; then
+ echo "✅ Pipeline run $run_id succeeded"
exit 0
else
- echo "❌ Pipeline run ${run_id} failed"
+ echo "❌ Pipeline run $run_id failed"
exit 1
fi
fi
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
- if [ ${elapsed_time} -ge ${timeout_seconds} ]; then
- echo "⏰ Timeout reached, pipeline run ${run_id} did not complete within ${timeout_seconds} seconds"
+ if [ $elapsed_time -ge $timeout_seconds ]; then
+ echo "⏰ Timeout reached, pipeline run $run_id did not complete within $timeout_seconds seconds"
- echo "Cancelling pipeline run ${run_id}"
+ echo "Cancelling pipeline run $run_id"
# TODO: Use Azure CLI to auhorize the pipeline to run on the agent pool (see: https://github.com/Azure/azure-devops-cli-extension/issues/876)
tmp_file=$(mktemp -t XXXXXX.json)
- cat < "${tmp_file}"
+ cat < "$tmp_file"
{
"status": "cancelling"
}
@@ -151,14 +164,14 @@ EOF
--api-version 7.1-preview \
--area build \
--http-method PATCH \
- --in-file "${tmp_file}" \
+ --in-file "$tmp_file" \
--resource builds \
- --route-parameters project=${project_id} buildId=${run_id} \
+ --route-parameters project=$project_id buildId=$run_id \
> /dev/null
exit 1
fi
- echo "Pipeline run ${run_id} is ${status}, retrying in 5 seconds"
+ echo "Pipeline run $run_id is $status, retrying in 5 seconds"
sleep 5
done
diff --git a/test/azure-devops/queue-cleaned.sh b/test/azure-devops/queue-cleaned.sh
new file mode 100644
index 00000000..c4208257
--- /dev/null
+++ b/test/azure-devops/queue-cleaned.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+agent="$1"
+pool_id="$2"
+
+if [ -z "$agent" ] || [ -z "$pool_id" ]; then
+ echo "Test is an Azure DevOps has been cleaned up from a pool."
+ echo "Usage: $1 $2 "
+ exit 1
+fi
+
+echo "➡️ Testing existence of agent $agent in pool $pool_id"
+
+# Wait for the agent ot be removed, as it is cleaned up asynchronously
+# TODO: Add a discriminator to the agent properties, like an environment variable, to ensure there is no test collision when running multiple tests in parallel from the same branch.
+echo "⏳ Waiting for the agent ot be removed"
+while true; do
+ agent_name=$(az pipelines agent list \
+ --pool-id "$pool_id" \
+ | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"$agent\")) and (.name | endswith(\"-template\") | not) and .status == \"offline\")).name // empty")
+ if [ -z "$agent_name" ]; then
+ echo "✅ Agent has been cleaned"
+ break
+ fi
+ echo "Agent still exists, retrying in 5 seconds"
+ sleep 5
+done
diff --git a/test/azure-devops/template-clean.sh b/test/azure-devops/template-clean.sh
new file mode 100644
index 00000000..6352be18
--- /dev/null
+++ b/test/azure-devops/template-clean.sh
@@ -0,0 +1,41 @@
+###
+# Remove the Azure DevOps template agent from a pool.
+#
+# Usage: ./template-clean.sh
+###
+
+#!/bin/bash
+set -e
+
+agent="$1"
+pool_id="$2"
+
+if [ -z "$agent" ] || [ -z "$pool_id" ]; then
+ echo "Remove the Azure DevOps template agent from a pool."
+ echo "Usage: $1 $2 "
+ exit 1
+fi
+
+echo "➡️ Removing template agent $agent from pool $pool_id"
+
+# Get the agent id
+agent_id=$(az pipelines agent list \
+ --pool-id "$pool_id" \
+ | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"$agent\")) and (.name | endswith(\"-template\")) and .status == \"offline\")).id // empty")
+
+# Fail if the agent does not exist
+if [ -z "$agent_id" ]; then
+ echo "❌ Template agent not found"
+ exit 1
+fi
+echo "Agent id: ${agent_id}"
+
+# Remove the agent
+az devops invoke \
+ --api-version "7.1" \
+ --area distributedtask \
+ --http-method DELETE \
+ --resource agents \
+ --route-parameters poolId="$pool_id" agentId="$agent_id"
+
+echo "✅ Agent removed"
diff --git a/test/azure-devops/template-exists.sh b/test/azure-devops/template-exists.sh
new file mode 100644
index 00000000..48e19a13
--- /dev/null
+++ b/test/azure-devops/template-exists.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+set -e
+
+agent="$1"
+pool_id="$2"
+
+if [ -z "$agent" ] || [ -z "$pool_id" ]; then
+ echo "Test the existence of a Azure DevOps template agent in a pool."
+ echo "Usage: $1 $2 "
+ exit 1
+fi
+
+echo "➡️ Testing existence of template agent $agent in pool $pool_id"
+
+# Wait for the agent to start
+echo "⏳ Waiting for the agent to start"
+while true; do
+ agent_json=$(az pipelines agent list \
+ --pool-id "$pool_id" \
+ | jq -r "last(sort_by(.createdOn) | .[] | select((.name | startswith(\"$agent\")) and (.name | endswith(\"-template\")) and .status == \"online\")) // empty")
+ if [ -n "$agent_json" ]; then
+ break
+ fi
+ echo "Not found, retrying in 5 seconds"
+ sleep 5
+done
+
+# Get the agent id
+agent_name=$(echo "$agent_json" | jq -r ".name")
+agent_id=$(echo "$agent_json" | jq -r ".id")
+echo "Agent id: $agent_id"
+
+# Get the agent capabilities
+agent_capabilities=$(az pipelines agent show \
+ --agent-id "$agent_id" \
+ --include-capabilities \
+ --pool-id "$pool_id" \
+ | jq -r ".systemCapabilities")
+echo "Capabilities:"
+echo $agent_capabilities | jq -r "to_entries | map(\"\(.key)=\(.value | tostring)\") | sort[]"
+
+echo "✅ Template agent found"
+
+echo "➡️ Testing automatic removal"
+
+# Wait for the agent to be offline
+echo "⏳ Waiting for the agent to be offline"
+while true; do
+ agent_status=$(az pipelines agent show \
+ --agent-id "$agent_id" \
+ --pool-id "$pool_id" \
+ | jq -r ".status")
+ if [ "$agent_status" == "offline" ]; then
+ break
+ fi
+ echo "Still online, retrying in 5 seconds"
+ sleep 5
+done
+
+echo "✅ Agent properly stopped"
diff --git a/test/integration-cleanup.sh b/test/integration-cleanup.sh
new file mode 100644
index 00000000..acf7e2dc
--- /dev/null
+++ b/test/integration-cleanup.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -e
+
+agent="$1"
+pool_name="$2"
+
+if [ -z "$agent" ] || [ -z "$pool_name" ]; then
+ echo "Clean up integration tests."
+ echo "Usage: $1 $2 "
+ exit 1
+fi
+
+echo "➡️ Running integration clean up for agent ${agent}"
+
+# Get the pool id
+pool_id=$(az pipelines pool list \
+ --pool-name "${pool_name}" \
+ --query "[0].id")
+
+# Fail if the pool does not exist
+if [ -z "$pool_id" ]; then
+ echo "❌ Pool ${pool_name} not found"
+ exit 1
+fi
+
+# Manually clean up the template agent
+# In a standard deployment, the agent would stay offline indefinitely
+bash test/azure-devops/template-clean.sh "${agent}" "${pool_id}"
+
+echo "✅ All clean up done"
diff --git a/test/integration-run.sh b/test/integration-run.sh
new file mode 100644
index 00000000..af5ac2ab
--- /dev/null
+++ b/test/integration-run.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+set -e
+
+prefix="$1"
+flavor="$2"
+version="$3"
+agent="$4"
+rg_name="$5"
+pool_name="$6"
+
+if [ -z "$prefix" ] || [ -z "$flavor" ] || [ -z "$version" ] || [ -z "$agent" ] || [ -z "$rg_name" ] || [ -z "$pool_name" ]; then
+ echo "Run all integration tests cases."
+ echo "Usage: $1 $2 $3 $4 $5 $6 "
+ exit 1
+fi
+
+echo "➡️ Running integration tests for agent ${agent} with prefix ${prefix}, flavor ${flavor} and version ${version}"
+
+echo "Configuring Azure DevOps organization ${org_url}"
+org_url="https://dev.azure.com/blue-agent"
+az devops configure --defaults organization=${org_url}
+
+# Get the pool id
+pool_id=$(az pipelines pool list \
+ --pool-name "${pool_name}" \
+ --query "[0].id")
+
+# Fail if the pool does not exist
+if [ -z "$pool_id" ]; then
+ echo "❌ Pool ${pool_name} not found"
+ exit 1
+fi
+echo "Pool id: ${pool_id}"
+
+# Test if template exists
+bash test/azure-devops/template-exists.sh "${agent}" "${pool_id}"
+
+# Run all integration tests in parallel
+parallel -j 0 bash test/azure-devops/pipeline.sh "${prefix}" {} "${flavor}" "${version}" ::: $(basename -s .yaml test/pipeline/*.yaml)
+
+# Check if any of the tests failed
+if [ $? -ne 0 ]; then
+ echo "❌ One or more integration tests failed"
+ exit 1
+fi
+
+# Test if all jobs were cleaned automatically
+bash test/azure-devops/queue-cleaned.sh "${agent}" "${pool_id}"
+
+# Test if the agent times out
+bash test/azure-devops/job-timeout.sh "${agent}" "${pool_id}" "${rg_name}"
+
+echo "✅ All integration tests passed"
diff --git a/test/integration.sh b/test/integration.sh
deleted file mode 100644
index 7f1f7127..00000000
--- a/test/integration.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/bash
-set -e
-
-prefix="$1"
-flavor="$2"
-version="$3"
-agent="$4"
-
-if [ -z "$prefix" ] || [ -z "$flavor" ] || [ -z "$version" ] || [ -z "$agent" ]; then
- echo "Run all integration tests cases."
- echo "Usage: $1 $2 $3 $4 "
- exit 1
-fi
-
-org_url="https://dev.azure.com/blue-agent"
-
-echo "Configuring Azure DevOps organization ${org_url}"
-az devops configure --defaults organization=${org_url}
-
-bash test/azure-devops/exists.sh "${agent}"
-
-# Run all integration tests in parallel
-pids=""
-for test in $(basename -s .yaml test/pipeline/*.yaml); do
- bash test/azure-devops/pipeline.sh "${prefix}" "${test}" "${flavor}" "${version}" &
- pids="$pids $!"
-done
-
-# Wait for all background jobs to complete
-for pid in $pids; do
- wait $pid || let "RESULT=1"
-done
-
-# Exit if any of them failed
-if [ "$RESULT" == "1" ]; then
- echo "One or more integration tests failed"
- exit 1
-fi
-
-bash test/azure-devops/has-been-cleaned.sh "${agent}"
diff --git a/test/pipeline/wait-5-minutes.yaml b/test/pipeline/wait-5-minutes.yaml
new file mode 100644
index 00000000..bec4c478
--- /dev/null
+++ b/test/pipeline/wait-5-minutes.yaml
@@ -0,0 +1,18 @@
+name: Wait 5 minutes
+
+parameters:
+ - name: flavor
+ type: string
+ - name: version
+ type: string
+
+jobs:
+ - job: test
+ pool:
+ name: github-actions
+ demands:
+ - flavor_${{ parameters.flavor }}
+ - version_${{ parameters.version }}
+ steps:
+ - bash: sleep 300
+ displayName: Wait for 5 minutes