diff --git a/.github/workflows/lambda.yml b/.github/workflows/lambda.yml index 8537e4beaa..6fa0af7393 100644 --- a/.github/workflows/lambda.yml +++ b/.github/workflows/lambda.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Install dependencies - run: yarn install --frozen-lockfile + run: yarn install --immutable --mode=skip-build - name: Run prettier run: yarn format-check @@ -77,24 +77,16 @@ jobs: uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Build scale-set service image - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - context: . - file: ./lambdas/services/scale-set/Dockerfile - platforms: linux/amd64,linux/arm64 - push: false - cache-from: type=gha,scope=scale-set-service - cache-to: type=gha,mode=max,scope=scale-set-service - - - name: Build scale-set service image for smoke test uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . file: ./lambdas/services/scale-set/Dockerfile platforms: linux/amd64 + push: false load: true tags: scale-set-service:smoke-test cache-from: type=gha,scope=scale-set-service + cache-to: type=gha,mode=max,scope=scale-set-service - name: Run scale-set service image smoke test - run: ./tests/scale-set-container-smoke-test.sh + run: ./tests/scale-set-container-smoke-test.sh \ No newline at end of file diff --git a/.github/workflows/packer-build.yml b/.github/workflows/packer-build.yml index 8dcff4efb6..dcb118c9dc 100644 --- a/.github/workflows/packer-build.yml +++ b/.github/workflows/packer-build.yml @@ -18,7 +18,6 @@ permissions: env: AWS_REGION: eu-west-1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: verify_packer: @@ -26,25 +25,68 @@ jobs: runs-on: ubuntu-latest container: image: index.docker.io/hashicorp/packer@sha256:12c441b8a3994e7df9f0e2692d9298f14c387e70bcc06139420977dbf80a137b # 1.11.2 - strategy: - matrix: - image: ["linux-al2023", "windows-core-2019", "windows-core-2022", "ubuntu-focal", "ubuntu-jammy", "ubuntu-jammy-arm64"] - defaults: - run: - working-directory: images/${{ matrix.image }} + env: + PACKER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: egress-policy: audit - - name: "Checkout" + - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + + - name: Discover Packer template directories + run: | + images="$( + find images \ + \( -type d \( -name .git -o -name .terraform \) -prune \) -o \ + \( -type f \( -name '*.pkr.hcl' -o -name '*.pkr.json' \) -print \) | + while IFS= read -r template; do + dirname "${template}" + done | + sort -u + )" + + if [ -z "${images}" ]; then + echo "::error::No Packer templates found under images/" + exit 1 + fi + + { + printf 'IMAGES<> "${GITHUB_ENV}" + - name: packer init - run: packer init . - - name: check packer formatting - run: packer fmt -recursive -check=true . - - name: packer validate - run: packer validate -evaluate-datasources . + run: | + printf '%s\n' "${IMAGES}" | while IFS= read -r image; do + [ -z "${image}" ] && continue + + echo "::group::Running packer init for image: ${image}" + (cd "${image}" && packer init .) + echo "::endgroup::" + done + + - name: Check packer formatting + run: | + printf '%s\n' "${IMAGES}" | while IFS= read -r image; do + [ -z "${image}" ] && continue + + echo "::group::Checking packer formatting for image: ${image}" + (cd "${image}" && packer fmt -recursive -check=true .) + echo "::endgroup::" + done + + - name: Validate packer + run: | + printf '%s\n' "${IMAGES}" | while IFS= read -r image; do + [ -z "${image}" ] && continue + + echo "::group::Validating image: ${image}" + (cd "${image}" && packer validate -evaluate-datasources .) + echo "::endgroup::" + done diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 8aeeba9944..b04006bbba 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -16,47 +16,6 @@ permissions: env: AWS_REGION: eu-west-1 - MODULES: | - . - modules/ami-housekeeper - modules/compute-providers/aws/ec2 - modules/compute-providers/aws/ec2/trust-policy - modules/download-lambda - modules/lambda - modules/multi-runner - modules/orchestration-providers/scale-set - modules/orchestration-providers/webhook - modules/orchestration-providers/webhook/job-retry - modules/orchestration-providers/webhook/pool - modules/orchestration-providers/webhook/scale-runners - modules/runner-binaries-syncer - modules/runner-config - modules/runner-config/ssm-housekeeper - modules/runners - modules/runners/job-retry - modules/runners/pool - modules/setup-iam-permissions - modules/storage-providers/aws/ssm - modules/termination-watcher - modules/termination-watcher/notification - modules/termination-watcher/termination - modules/webhook - modules/webhook/direct - modules/webhook/eventbridge - modules/webhook-github-app - EXAMPLES: | - default - prebuilt - ephemeral - termination-watcher - multi-runner - multi-runner-v2 - multi-runner-scale-set - external-managed-ssm-secrets - TEST_MODULES: | - modules/runners - modules/multi-runner - modules/orchestration-providers/scale-set jobs: verify_modules: @@ -127,6 +86,32 @@ jobs: tofu_version: ${{ matrix.iac.version }} tofu_wrapper: false + - name: Discover Terraform modules + shell: bash + run: | + modules="$( + { + printf '.\n' + find modules \ + \( -type d \( -name .git -o -name .terraform -o -name tests \) -prune \) -o \ + \( -type f -name '*.tf' -print \) | + while IFS= read -r tf_file; do + dirname "${tf_file}" + done + } | sort -u + )" + + if [ -z "${modules}" ]; then + echo "::error::No Terraform modules found" + exit 1 + fi + + { + printf 'MODULES<> "${GITHUB_ENV}" + - name: ${{ matrix.iac.name }} init env: IAC_COMMAND: ${{ matrix.iac.command }} @@ -254,58 +239,83 @@ jobs: tofu_version: ${{ matrix.iac.version }} tofu_wrapper: false + - name: Discover examples + shell: bash + run: | + examples="$( + find examples \ + \( -type d \( -name .git -o -name .terraform \) -prune \) -o \ + \( -type f -name '*.tf' -print \) | + while IFS= read -r tf_file; do + dirname "${tf_file}" + done | + sed 's#^examples/##' | + sort -u + )" + + if [ -z "${examples}" ]; then + echo "::error::No Terraform examples found" + exit 1 + fi + + { + printf 'EXAMPLES<> "${GITHUB_ENV}" + - name: Select ${{ matrix.iac.name }} lockfile if: matrix.iac.command == 'tofu' env: IAC_LOCK_FILE: ${{ matrix.iac.lockfile }} run: | - printf '%s\n' "${EXAMPLES}" | while IFS= read -r example; do + while IFS= read -r example; do [ -z "${example}" ] && continue echo "::group::Selecting lockfile for example: ${example}" cp "examples/${example}/${IAC_LOCK_FILE}" \ "examples/${example}/.terraform.lock.hcl" echo "::endgroup::" - done + done <<< "${EXAMPLES}" - name: ${{ matrix.iac.name }} init env: IAC_COMMAND: ${{ matrix.iac.command }} run: | - printf '%s\n' "${EXAMPLES}" | while IFS= read -r example; do + while IFS= read -r example; do [ -z "${example}" ] && continue echo "::group::Running $IAC_COMMAND init for example: ${example}" $IAC_COMMAND -chdir="examples/${example}" init \ -get -backend=false -input=false -lockfile=readonly echo "::endgroup::" - done + done <<< "${EXAMPLES}" - name: Check ${{ matrix.iac.name }} formatting env: IAC_COMMAND: ${{ matrix.iac.command }} run: | - printf '%s\n' "${EXAMPLES}" | while IFS= read -r example; do + while IFS= read -r example; do [ -z "${example}" ] && continue echo "::group::Checking $IAC_COMMAND formatting for example: ${example}" $IAC_COMMAND -chdir="examples/${example}" fmt \ -recursive -check=true -write=false echo "::endgroup::" - done + done <<< "${EXAMPLES}" continue-on-error: ${{ matrix.iac.version == 'latest' }} - name: Validate ${{ matrix.iac.name }} env: IAC_COMMAND: ${{ matrix.iac.command }} run: | - printf '%s\n' "${EXAMPLES}" | while IFS= read -r example; do + while IFS= read -r example; do [ -z "${example}" ] && continue echo "::group::Validating example: ${example}" $IAC_COMMAND -chdir="examples/${example}" validate echo "::endgroup::" - done + done <<< "${EXAMPLES}" - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 name: Cache TFLint plugin dir @@ -322,7 +332,7 @@ jobs: run: | tflint --init -c ${GITHUB_WORKSPACE}/.tflint.hcl - printf '%s\n' "${EXAMPLES}" | while IFS= read -r example; do + while IFS= read -r example; do [ -z "${example}" ] && continue echo "::group::Running TFLint for example: ${example}" @@ -331,7 +341,7 @@ jobs: --var-file ${GITHUB_WORKSPACE}/.github/lint/tflint.tfvars \ --chdir "examples/${example}" echo "::endgroup::" - done + done <<< "${EXAMPLES}" terraform_test: name: ${{ matrix.iac.name }} test @@ -387,27 +397,52 @@ jobs: tofu_version: ${{ matrix.iac.version }} tofu_wrapper: false + - name: Discover Terraform test modules + shell: bash + run: | + test_modules="$( + find . \ + \( -type d \( -name .git -o -name .terraform \) -prune \) -o \ + \( -type f -name '*.tftest.hcl' -print \) | + while IFS= read -r test_file; do + test_dir=$(dirname "${test_file}") + printf '%s\n' "${test_dir%/tests}" + done | + sort -u + )" + + if [ -z "${test_modules}" ]; then + echo "::error::No Terraform test modules found" + exit 1 + fi + + { + printf 'TEST_MODULES<> "${GITHUB_ENV}" + - name: ${{ matrix.iac.name }} init env: IAC_COMMAND: ${{ matrix.iac.command }} run: | - printf '%s\n' "${TEST_MODULES}" | while IFS= read -r module; do + while IFS= read -r module; do [ -z "${module}" ] && continue echo "::group::Running $IAC_COMMAND init for test module: ${module}" $IAC_COMMAND -chdir="${module}" init \ -backend=false -input=false echo "::endgroup::" - done + done <<< "${TEST_MODULES}" - name: ${{ matrix.iac.name }} test env: IAC_COMMAND: ${{ matrix.iac.command }} run: | - printf '%s\n' "${TEST_MODULES}" | while IFS= read -r module; do + while IFS= read -r module; do [ -z "${module}" ] && continue echo "::group::Running $IAC_COMMAND test for module: ${module}" $IAC_COMMAND -chdir="${module}" test -test-directory=tests -compact-warnings echo "::endgroup::" - done + done <<< "${TEST_MODULES}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1502b6ab8e..9c63b9a806 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,26 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.96.2 + rev: 581213484f4262600d17c71e272176d4eb6724a5 # frozen: v1.109.0 hooks: - id: terraform_fmt - id: terraform_tflint args: - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl --var-file __GIT_WORKING_DIR__/.github/lint/tflint.tfvars + - id: terraform_validate + name: Terraform · Validate + always_run: true + args: + - --hook-config=--retry-once-with-cleanup=true + - --tf-init-args=-backend=false + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-merge-conflict + + - repo: https://github.com/hadolint/hadolint + rev: 2eece55955ced00200be9729e9728cb7dacca505 # frozen: v2.15.1 + hooks: + - id: hadolint + name: Docker · Linter + exclude: (docs/operations/repo-blueprints/) \ No newline at end of file diff --git a/images/linux-al2023/github_agent.linux.pkr.hcl b/images/linux-al2023/github_agent.linux.pkr.hcl index 2876f053c7..d5fbb289cd 100644 --- a/images/linux-al2023/github_agent.linux.pkr.hcl +++ b/images/linux-al2023/github_agent.linux.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -101,10 +108,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/images/ubuntu-focal/github_agent.ubuntu.pkr.hcl b/images/ubuntu-focal/github_agent.ubuntu.pkr.hcl index 9712417274..2a84637af1 100644 --- a/images/ubuntu-focal/github_agent.ubuntu.pkr.hcl +++ b/images/ubuntu-focal/github_agent.ubuntu.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -91,10 +98,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/images/ubuntu-jammy-arm64/github_agent.ubuntu.pkr.hcl b/images/ubuntu-jammy-arm64/github_agent.ubuntu.pkr.hcl index 1536eb5784..69df5554d8 100644 --- a/images/ubuntu-jammy-arm64/github_agent.ubuntu.pkr.hcl +++ b/images/ubuntu-jammy-arm64/github_agent.ubuntu.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -91,10 +98,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/images/ubuntu-jammy/github_agent.ubuntu.pkr.hcl b/images/ubuntu-jammy/github_agent.ubuntu.pkr.hcl index be23a256ca..d8d52ea9d1 100644 --- a/images/ubuntu-jammy/github_agent.ubuntu.pkr.hcl +++ b/images/ubuntu-jammy/github_agent.ubuntu.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -91,10 +98,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/images/windows-core-2019/github_agent.windows.pkr.hcl b/images/windows-core-2019/github_agent.windows.pkr.hcl index e27ad4a2bc..73de75d3fd 100644 --- a/images/windows-core-2019/github_agent.windows.pkr.hcl +++ b/images/windows-core-2019/github_agent.windows.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -56,10 +63,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/images/windows-core-2022/github_agent.windows.pkr.hcl b/images/windows-core-2022/github_agent.windows.pkr.hcl index 0a85595ec5..802639f248 100644 --- a/images/windows-core-2022/github_agent.windows.pkr.hcl +++ b/images/windows-core-2022/github_agent.windows.pkr.hcl @@ -7,6 +7,13 @@ packer { } } +variable "github_api_token" { + description = "Optional GitHub token for the runner release API." + type = string + default = env("PACKER_GITHUB_API_TOKEN") + sensitive = true +} + variable "runner_version" { description = "The version (no v prefix) of the runner software to install https://github.com/actions/runner/releases. The latest release will be fetched from GitHub if not provided." default = null @@ -73,10 +80,15 @@ variable "temporary_security_group_source_public_ip" { data "http" github_runner_release_json { url = "https://api.github.com/repos/actions/runner/releases/latest" - request_headers = { - Accept = "application/vnd.github+json" - X-GitHub-Api-Version : "2022-11-28" - } + request_headers = merge( + { + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + }, + var.github_api_token == "" ? {} : { + Authorization = "Bearer ${var.github_api_token}" + } + ) } locals { diff --git a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl index a8258163b8..d1165397e8 100644 --- a/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl +++ b/modules/compute-providers/aws/ec2/tests/provider.tftest.hcl @@ -95,7 +95,13 @@ run "separates_control_plane_contract_from_ec2_resources" { command = plan assert { - condition = toset(keys(output.provider)) == toset(["environment_variables", "policies", "resources"]) + condition = toset(keys(output.provider)) == toset([ + "capabilities", + "environment_variables", + "policies", + "resources", + "type", + ]) error_message = "The EC2 provider contract must expose only integration and resource data; its module identity must not be repeated in the output." } @@ -216,11 +222,18 @@ run "includes_managed_ami_read_in_scale_set_contract" { command = plan variables { - config = merge(var.config, { - ami = merge(var.config.ami, { + config = { + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678"] + instance_types = ["m5.large"] + binaries_syncer = { + enabled = false + s3 = null + } + ami = { id_ssm_parameter = null - }) - }) + } + } } assert { diff --git a/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf b/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf index bea81c1b9e..7be78e1c17 100644 --- a/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf +++ b/modules/compute-providers/aws/ec2/trust-policy/assume-role.tf @@ -13,6 +13,8 @@ data "aws_iam_policy_document" "default" { data "aws_iam_policy_document" "assume_role" { source_policy_documents = compact([ data.aws_iam_policy_document.default.json, - var.additional_trust_policy_json, + var.additional_trust_policy_json != null && can(jsondecode(var.additional_trust_policy_json)) + ? var.additional_trust_policy_json + : null, ]) } diff --git a/modules/multi-runner/tests/config-resolution.tftest.hcl b/modules/multi-runner/tests/config-resolution.tftest.hcl index 9d4e913456..425717f5c6 100644 --- a/modules/multi-runner/tests/config-resolution.tftest.hcl +++ b/modules/multi-runner/tests/config-resolution.tftest.hcl @@ -450,6 +450,10 @@ run "v2_inputs_do_not_require_legacy_arguments" { } } } + runner = { + os = "linux" + architecture = "x64" + } compute_provider = { aws = { ec2 = { @@ -804,6 +808,10 @@ run "scale_set_lane_requires_owner_for_non_enterprise_registration" { name = "scale-missing-owner" } } + runner = { + os = "linux" + architecture = "x64" + } compute_provider = { aws = { ec2 = { diff --git a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl index 0e797b9a04..3a1c41abfb 100644 --- a/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl +++ b/modules/orchestration-providers/webhook/job-retry/tests/job-retry.tftest.hcl @@ -11,6 +11,13 @@ mock_provider "aws" { } } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:mock-queue" + } + } + } variables { @@ -230,6 +237,14 @@ run "does_not_enable_partial_vpc_configuration" { command = plan variables { + storage_provider = { + aws = { + ssm = { + kms_key_id = null + } + } + } + config = { prefix = "job-retry-test" aws_partition = "aws" @@ -341,11 +356,76 @@ run "rejects_unsupported_lambda_architecture" { } variables { - config = merge(var.config, { - lambda = merge(var.config.lambda, { - architecture = "unsupported" - }) - }) + config = { + prefix = "job-retry-test" + aws_partition = "aws" + lambda = { + artifact = { + zip = "unused.zip" + s3 = { + bucket = "lambda-artifacts" + key = "job-retry.zip" + } + } + architecture = "unsupported" + runtime = "nodejs24.x" + memory_size = 256 + timeout = 30 + reserved_concurrent_executions = 1 + environment_variables = {} + vpc = { + subnet_ids = [] + security_group_ids = [] + } + role = { + path = "/job-retry-test/" + principals = [] + } + } + runner = { name_prefix = "required-prefix-" } + github = { + organization_runners = false + enterprise_server = { url = null, ssl_verify = false } + app_parameters = { key_base64 = {}, id = {} } + } + queue = { + build = { + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + } + event_source_mapping = { + batch_size = 10 + maximum_batching_window_in_seconds = 0 + } + encryption = { sqs_managed_sse_enabled = true } + } + observability = { + logs = { + level = "trace" + retention_in_days = 14 + class = "STANDARD" + } + tracing = { + capture_http_requests = false + capture_error = false + } + metrics = { + enabled = false + namespace = "JobRetryTest" + metric = { + github_app_rate_limit = { enabled = false } + job_retry = { enabled = false } + } + } + } + tags = { + resources = {} + lambda = {} + log_group = {} + queue = {} + event_source_mapping = {} + } + } } expect_failures = [terraform_data.validate_config] @@ -359,13 +439,76 @@ run "rejects_unsupported_log_level" { } variables { - config = merge(var.config, { - observability = merge(var.config.observability, { - logs = merge(var.config.observability.logs, { - level = "verbose" - }) - }) - }) + config = { + prefix = "job-retry-test" + aws_partition = "aws" + lambda = { + artifact = { + zip = "unused.zip" + s3 = { + bucket = "lambda-artifacts" + key = "job-retry.zip" + } + } + architecture = "arm64" + runtime = "nodejs24.x" + memory_size = 256 + timeout = 30 + reserved_concurrent_executions = 1 + environment_variables = {} + vpc = { + subnet_ids = [] + security_group_ids = [] + } + role = { + path = "/job-retry-test/" + principals = [] + } + } + runner = { name_prefix = "required-prefix-" } + github = { + organization_runners = false + enterprise_server = { url = null, ssl_verify = false } + app_parameters = { key_base64 = {}, id = {} } + } + queue = { + build = { + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + } + event_source_mapping = { + batch_size = 10 + maximum_batching_window_in_seconds = 0 + } + encryption = { sqs_managed_sse_enabled = true } + } + observability = { + logs = { + level = "verbose" + retention_in_days = 14 + class = "STANDARD" + } + tracing = { + capture_http_requests = false + capture_error = false + } + metrics = { + enabled = false + namespace = "JobRetryTest" + metric = { + github_app_rate_limit = { enabled = false } + job_retry = { enabled = false } + } + } + } + tags = { + resources = {} + lambda = {} + log_group = {} + queue = {} + event_source_mapping = {} + } + } } expect_failures = [terraform_data.validate_config] @@ -379,9 +522,76 @@ run "rejects_resource_prefix_longer_than_aws_limit" { } variables { - config = merge(var.config, { - prefix = "1234567890123456789012345678901234567890123456789012345" - }) + config = { + prefix = "1234567890123456789012345678901234567890123456789012345" + aws_partition = "aws" + lambda = { + artifact = { + zip = "unused.zip" + s3 = { + bucket = "lambda-artifacts" + key = "job-retry.zip" + } + } + architecture = "arm64" + runtime = "nodejs24.x" + memory_size = 256 + timeout = 30 + reserved_concurrent_executions = 1 + environment_variables = {} + vpc = { + subnet_ids = [] + security_group_ids = [] + } + role = { + path = "/job-retry-test/" + principals = [] + } + } + runner = { name_prefix = "required-prefix-" } + github = { + organization_runners = false + enterprise_server = { url = null, ssl_verify = false } + app_parameters = { key_base64 = {}, id = {} } + } + queue = { + build = { + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + } + event_source_mapping = { + batch_size = 10 + maximum_batching_window_in_seconds = 0 + } + encryption = { sqs_managed_sse_enabled = true } + } + observability = { + logs = { + level = "trace" + retention_in_days = 14 + class = "STANDARD" + } + tracing = { + capture_http_requests = false + capture_error = false + } + metrics = { + enabled = false + namespace = "JobRetryTest" + metric = { + github_app_rate_limit = { enabled = false } + job_retry = { enabled = false } + } + } + } + tags = { + resources = {} + lambda = {} + log_group = {} + queue = {} + event_source_mapping = {} + } + } } expect_failures = [terraform_data.validate_config] diff --git a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl index 25fa6c8a14..5d102cf7be 100644 --- a/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl +++ b/modules/orchestration-providers/webhook/pool/tests/provider.tftest.hcl @@ -4,6 +4,19 @@ mock_provider "aws" { json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"logs:CreateLogStream\",\"Resource\":\"*\"}]}" } } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/pool-test" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:mock-function" + } + } + } variables { @@ -228,9 +241,18 @@ run "omits_optional_kms_statement" { command = plan variables { - config = merge(var.config, { - kms_key_id = null - }) + storage_provider = { + aws = { + ssm = { + token_path = "/github-runner/tokens" + token_path_arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/tokens" + config_path = "/github-runner/config" + config_path_arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config" + kms_key_id = null + parameter_store_tags = "{}" + } + } + } } assert { @@ -253,9 +275,13 @@ run "rejects_empty_compute_provider_type" { } variables { - runner_provider = merge(var.runner_provider, { - type = " " - }) + runner_provider = { + type = " " + environment_variables = { MICROVM_CLUSTER = "runner-cluster" } + iam_policy_json = jsonencode({ Version = "2012-10-17", Statement = [] }) + managed_policy_enabled = true + managed_policy_arn = "arn:aws:iam::123456789012:policy/microvm-pool" + } } expect_failures = [terraform_data.validate_config] @@ -269,9 +295,13 @@ run "rejects_invalid_compute_provider_policy" { } variables { - runner_provider = merge(var.runner_provider, { - iam_policy_json = "not-json" - }) + runner_provider = { + type = "microvm" + environment_variables = { MICROVM_CLUSTER = "runner-cluster" } + iam_policy_json = "not-json" + managed_policy_enabled = true + managed_policy_arn = "arn:aws:iam::123456789012:policy/microvm-pool" + } } expect_failures = [terraform_data.validate_config] @@ -285,10 +315,13 @@ run "requires_enabled_compute_provider_managed_policy_arn" { } variables { - runner_provider = merge(var.runner_provider, { + runner_provider = { + type = "microvm" + environment_variables = { MICROVM_CLUSTER = "runner-cluster" } + iam_policy_json = jsonencode({ Version = "2012-10-17", Statement = [] }) managed_policy_enabled = true managed_policy_arn = null - }) + } } expect_failures = [terraform_data.validate_config] diff --git a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl index 22b695aac2..d4171ca2d7 100644 --- a/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl +++ b/modules/orchestration-providers/webhook/scale-runners/tests/scale-runners.tftest.hcl @@ -10,6 +10,19 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/scale-runners-test" } } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:mock-function" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws-us-gov:events:us-gov-west-1:123456789012:rule/mock-event-rule" + } + } + } variables { @@ -409,18 +422,87 @@ run "omits_optional_kms_statements" { command = plan variables { - config = merge(var.config, { - queue = merge(var.config.queue, { - kms_key_id = null - }) - }) - storage_provider = merge(var.storage_provider, { - aws = merge(var.storage_provider.aws, { - ssm = merge(var.storage_provider.aws.ssm, { - kms_key_id = null - }) - }) - }) + config = { + prefix = "scale-runners-test" + lambda = { + artifact = { zip = "runners.zip", s3 = { bucket = "lambda-artifacts", key = "runners.zip" } } + runtime = "nodejs24.x" + architecture = "arm64" + vpc = { subnet_ids = [], security_group_ids = [] } + role = { path = "/scale-runners-test/" } + } + runner = { + os = "linux" + auto_update_disabled = false + ephemeral = true + labels = ["self-hosted", "linux"] + group_name = "default" + name_prefix = "test-" + boot_time_in_minutes = 10 + maximum_count = 10 + } + github = { + organization_runners = true + enterprise_server = { url = null, ssl_verify = true } + app_parameters = { + key_base64 = { + name = "/github-runner/key-base64" + arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64" + } + id = { + name = "/github-runner/app-id" + arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id" + } + } + } + queue = { + build = { arn = "arn:aws-us-gov:sqs:us-gov-west-1:123456789012:build-queue" } + kms_key_id = null + event_source_mapping = { batch_size = 10, maximum_batching_window_in_seconds = 0 } + } + observability = { + logs = { level = "info", retention_in_days = 14, class = "STANDARD" } + tracing = { capture_http_requests = false, capture_error = false } + metrics = { + enabled = false + namespace = "ScaleRunnersTest" + metric = { github_app_rate_limit = { enabled = false } } + } + } + scale_up = { + memory_size = 512 + timeout = 60 + reserved_concurrent_executions = 1 + job_queued_check_enabled = false + tags = { resources = {}, lambda = {}, log_group = {}, event_source_mapping = {} } + } + scale_down = { + memory_size = 512 + timeout = 60 + schedule_expression = "rate(10 minutes)" + idle_config = [] + tags = { resources = {}, lambda = {}, log_group = {} } + } + job_retry = { + enabled = false + max_attempts = 3 + delay_in_seconds = 60 + delay_backoff = 1 + queue = null + } + } + storage_provider = { + aws = { + ssm = { + token_path = "/github-runner/tokens" + token_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/tokens" + config_path = "/github-runner/config" + config_path_arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/config" + parameter_store_tags = "[]" + kms_key_id = null + } + } + } } assert { @@ -448,12 +530,75 @@ run "requires_job_retry_queue_when_enabled" { } variables { - config = merge(var.config, { - job_retry = merge(var.config.job_retry, { - enabled = true - queue = null - }) - }) + config = { + prefix = "scale-runners-test" + lambda = { + artifact = { zip = "runners.zip", s3 = { bucket = "lambda-artifacts", key = "runners.zip" } } + runtime = "nodejs24.x" + architecture = "arm64" + vpc = { subnet_ids = [], security_group_ids = [] } + role = { path = "/scale-runners-test/" } + } + runner = { + os = "linux" + auto_update_disabled = false + ephemeral = true + labels = ["self-hosted", "linux"] + group_name = "default" + name_prefix = "test-" + boot_time_in_minutes = 10 + maximum_count = 10 + } + github = { + organization_runners = true + enterprise_server = { url = null, ssl_verify = true } + app_parameters = { + key_base64 = { + name = "/github-runner/key-base64" + arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/key-base64" + } + id = { + name = "/github-runner/app-id" + arn = "arn:aws-us-gov:ssm:us-gov-west-1:123456789012:parameter/github-runner/app-id" + } + } + } + queue = { + build = { arn = "arn:aws-us-gov:sqs:us-gov-west-1:123456789012:build-queue" } + kms_key_id = null + event_source_mapping = { batch_size = 10, maximum_batching_window_in_seconds = 0 } + } + observability = { + logs = { level = "info", retention_in_days = 14, class = "STANDARD" } + tracing = { capture_http_requests = false, capture_error = false } + metrics = { + enabled = false + namespace = "ScaleRunnersTest" + metric = { github_app_rate_limit = { enabled = false } } + } + } + scale_up = { + memory_size = 512 + timeout = 60 + reserved_concurrent_executions = 1 + job_queued_check_enabled = false + tags = { resources = {}, lambda = {}, log_group = {}, event_source_mapping = {} } + } + scale_down = { + memory_size = 512 + timeout = 60 + schedule_expression = "rate(10 minutes)" + idle_config = [] + tags = { resources = {}, lambda = {}, log_group = {} } + } + job_retry = { + enabled = true + max_attempts = 3 + delay_in_seconds = 60 + delay_backoff = 1 + queue = null + } + } } expect_failures = [terraform_data.validate_config] diff --git a/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl b/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl index 5d8ca6a68a..31f198c2ce 100644 --- a/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl +++ b/modules/orchestration-providers/webhook/tests/webhook.tftest.hcl @@ -10,6 +10,25 @@ mock_provider "aws" { arn = "arn:aws:iam::123456789012:role/webhook-orchestration-test" } } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:mock-function" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:mock-queue" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/mock-event-rule" + } + } + } variables { @@ -305,13 +324,55 @@ run "rejects_conflicting_artifact_sources" { } variables { - config = merge(var.config, { - lambda = merge(var.config.lambda, { - artifact = merge(var.config.lambda.artifact, { - zip = "runners.zip" - }) - }) - }) + config = { + runner = { + boot_time_in_minutes = 11 + ephemeral = true + maximum_count = 10 + } + github = { organization_runners = true } + queue = { + build = { + arn = "arn:aws:sqs:eu-west-1:123456789012:build-queue" + url = "https://sqs.eu-west-1.amazonaws.com/123456789012/build-queue" + } + } + lambda = { + artifact = { zip = "runners.zip", s3 = { key = "runners.zip" } } + scale = { + up = { + memory_size = 512 + timeout = 60 + reserved_concurrent_executions = 1 + event_source_mapping = { batch_size = 10, maximum_batching_window_in_seconds = 0 } + } + down = { + memory_size = 512 + timeout = 60 + schedule_expression = "rate(5 minutes)" + idle_config = [] + } + } + pool = { + memory_size = 512 + timeout = 60 + reserved_concurrent_executions = 1 + config = [] + include_busy_runners = false + } + } + job_retry = { + enabled = false + delay_in_seconds = 60 + delay_backoff = 1 + max_attempts = 3 + lambda = { + memory_size = 256 + reserved_concurrent_executions = 1 + timeout = 30 + } + } + } } expect_failures = [terraform_data.validate_config] diff --git a/modules/runner-config/compute-provider.tf b/modules/runner-config/compute-provider.tf index bffc43b814..d9bd644d6b 100644 --- a/modules/runner-config/compute-provider.tf +++ b/modules/runner-config/compute-provider.tf @@ -3,11 +3,10 @@ locals { aws_ec2 = var.compute_provider.aws.ec2 } - discovered_provider_key = one([ + provider_key = one([ for provider_key, provider_config in local.compute_providers : provider_key if provider_config != null ]) - provider_key = var.compute_provider_key != null ? var.compute_provider_key : local.discovered_provider_key provider_types = { aws_ec2 = "ec2" diff --git a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl index 3e08eeb84b..c2834a5e63 100644 --- a/modules/runner-config/tests/computed-iam-inputs.tftest.hcl +++ b/modules/runner-config/tests/computed-iam-inputs.tftest.hcl @@ -4,6 +4,37 @@ mock_provider "aws" { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" } } + + mock_resource "aws_iam_role" { + defaults = { + arn = "arn:aws:iam::123456789012:role/runner-config-test" + } + } + + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/mock-policy" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:mock-function" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:mock-queue" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/mock-event-rule" + } + } + } run "computed_external_values_keep_plan_shape_known" { diff --git a/modules/runner-config/tests/pool.tftest.hcl b/modules/runner-config/tests/pool.tftest.hcl index f0cb0048e4..8537f3f553 100644 --- a/modules/runner-config/tests/pool.tftest.hcl +++ b/modules/runner-config/tests/pool.tftest.hcl @@ -16,6 +16,31 @@ mock_provider "aws" { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id" } } + + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/mock-policy" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:mock-function" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:mock-queue" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/mock-event-rule" + } + } + } # The runner archive is injected during packaging, so isolate the common @@ -33,6 +58,7 @@ variables { vpc_id = "vpc-12345678" subnet_ids = ["subnet-12345678"] instance_types = ["m5.large"] + ssm_enabled = true ami = { filter = { state = ["available"] } owners = ["amazon"] @@ -605,11 +631,7 @@ run "rejects_empty_compute_provider" { compute_provider = {} } - plan_options { - target = [terraform_data.validate_config] - } - - expect_failures = [terraform_data.validate_config] + expect_failures = [var.compute_provider] } run "rejects_empty_aws_compute_provider_namespace" { @@ -621,11 +643,7 @@ run "rejects_empty_aws_compute_provider_namespace" { } } - plan_options { - target = [terraform_data.validate_config] - } - - expect_failures = [terraform_data.validate_config] + expect_failures = [var.compute_provider] } run "job_retry_uses_common_runner_configuration_identity" { diff --git a/modules/runner-config/tests/tags.tftest.hcl b/modules/runner-config/tests/tags.tftest.hcl index 8f9cbb2d0d..fe98663654 100644 --- a/modules/runner-config/tests/tags.tftest.hcl +++ b/modules/runner-config/tests/tags.tftest.hcl @@ -16,6 +16,31 @@ mock_provider "aws" { arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/config" } } + + mock_resource "aws_iam_policy" { + defaults = { + arn = "arn:aws:iam::123456789012:policy/mock-policy" + } + } + + mock_resource "aws_lambda_function" { + defaults = { + arn = "arn:aws:lambda:eu-west-1:123456789012:function:mock-function" + } + } + + mock_resource "aws_sqs_queue" { + defaults = { + arn = "arn:aws:sqs:eu-west-1:123456789012:mock-queue" + } + } + + mock_resource "aws_cloudwatch_event_rule" { + defaults = { + arn = "arn:aws:events:eu-west-1:123456789012:rule/mock-event-rule" + } + } + } # The runner archive is injected during packaging, so model the common diff --git a/modules/runner-config/validations.tf b/modules/runner-config/validations.tf index df0671e93f..023032fafc 100644 --- a/modules/runner-config/validations.tf +++ b/modules/runner-config/validations.tf @@ -77,14 +77,6 @@ resource "terraform_data" "validate_config" { error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2." } - precondition { - condition = var.compute_provider_key == null || try( - local.compute_providers[var.compute_provider_key] != null, - false, - ) - error_message = "compute_provider_key must identify the non-null typed compute-provider block." - } - precondition { condition = length([ for provider_name, provider_config in var.orchestration_provider : provider_name diff --git a/modules/runner-config/variables.compute-provider.tf b/modules/runner-config/variables.compute-provider.tf index 954bc18c61..d7aaaafe2a 100644 --- a/modules/runner-config/variables.compute-provider.tf +++ b/modules/runner-config/variables.compute-provider.tf @@ -1,16 +1,3 @@ -# Optional plan-known dispatch key supplied by the multi-runner topology layer. -variable "compute_provider_key" { - description = "Optional plan-known compute-provider dispatch key. Null discovers the key from the exactly one populated compute_provider block." - type = string - default = null - - validation { - condition = var.compute_provider_key == null ? true : contains(["aws_ec2"], var.compute_provider_key) - error_message = "compute_provider_key must be null or aws_ec2." - } -} - -# Typed compute-provider input boundary between the common control plane and compute implementations. variable "compute_provider" { description = <<-EOT Typed compute-provider configuration. Provider-owned settings remain inside the selected compute-provider block. @@ -294,4 +281,8 @@ variable "compute_provider" { }), {}) }) + validation { + condition = var.compute_provider.aws.ec2 != null + error_message = "Exactly one compute-provider block must be set. Supported compute-provider blocks: aws.ec2." + } }