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