From cc30659f5c8a0c74781058d56aed97481992528c Mon Sep 17 00:00:00 2001 From: Michael Li Date: Fri, 20 Mar 2026 14:31:48 -0400 Subject: [PATCH] chore(e2e): Add workflow for running admin ui tests --- .github/workflows/build.yml | 24 +++ .github/workflows/enos-run-admin-ui.yml | 244 ++++++++++++++++++++++++ enos/enos-scenario-e2e-ui-aws.hcl | 47 ++--- enos/enos-scenario-e2e-ui-docker.hcl | 51 +++-- enos/modules/test_e2e_ui/main.tf | 16 +- internal/ui/VERSION | 2 +- 6 files changed, 314 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/enos-run-admin-ui.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81d6ed1fa5..f9de8b6066 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -414,6 +414,30 @@ jobs: edition: ${{ needs.product-metadata.outputs.product-edition }} docker-image-file: "boundary_default_linux_amd64_${{ needs.set-product-version.outputs.product-version }}_${{ github.sha }}.docker.dev.tar" secrets: inherit + + e2e-admin-ui: + name: e2e-admin-ui + # Only run this workflow on pull requests that have been originated from + # the hashicorp/boundary repository. As Enos scenarios require access to + # Github Actions secrets, it only makes sense to run this workflow when those + # secrets are available. Any pull requests from forks will not trigger the + # workflow. + if: | + github.event.pull_request.head.repo.fork != 'true' && ( + (github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/')) || + (github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/')) + ) + needs: + - set-product-version + - product-metadata + - build-docker + uses: ./.github/workflows/enos-run-admin-ui.yml + with: + artifact-name: "boundary_${{ needs.set-product-version.outputs.product-version }}_linux_amd64.zip" + edition: ${{ needs.product-metadata.outputs.product-edition }} + docker-image-file: "boundary_default_linux_amd64_${{ needs.set-product-version.outputs.product-version }}_${{ github.sha }}.docker.dev.tar" + secrets: inherit + bats: uses: ./.github/workflows/test-cli-ui_oss.yml if: github.event.pull_request.head.repo.fork != 'true' diff --git a/.github/workflows/enos-run-admin-ui.yml b/.github/workflows/enos-run-admin-ui.yml new file mode 100644 index 0000000000..35c04388f8 --- /dev/null +++ b/.github/workflows/enos-run-admin-ui.yml @@ -0,0 +1,244 @@ +--- +name: Run Admin UI E2E Tests + +on: + # Only trigger this working using workflow_call. It assumes that secrets are + # being inherited from the caller. + workflow_call: + inputs: + artifact-name: + required: true + type: string + edition: + required: true + type: string + docker-image-file: + required: false + type: string + +jobs: + test: + runs-on: ${{ fromJSON(vars.RUNNER_LARGE) }} + continue-on-error: true + strategy: + fail-fast: false # don't fail as that can skip required cleanup steps for jobs + matrix: + include: + - filter: 'e2e_ui_aws builder:crt protocol:http' + infra: 'aws' + - filter: 'e2e_ui_docker builder:crt' + infra: 'docker' + env: + GITHUB_TOKEN: ${{ secrets.SERVICE_USER_GITHUB_TOKEN }} + # Scenario variables + ENOS_VAR_aws_region: us-east-1 + ENOS_VAR_aws_ssh_keypair_name: ${{ github.event.repository.name }}-ci-ssh-key + ENOS_VAR_aws_ssh_private_key_path: ./support/private_key.pem + ENOS_VAR_crt_bundle_path: ./support/boundary.zip + ENOS_VAR_test_email: ${{ secrets.SERVICE_USER_EMAIL }} + ENOS_VAR_boundary_edition: ${{ inputs.edition }} + ENOS_VAR_boundary_docker_image_file: ./support/boundary_docker_image.tar + steps: + - name: Set git config + run: | + git config --global url."https://oauth2:${{ secrets.SERVICE_USER_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: '0' + + - name: Set up Node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + package-manager-cache: false + + - name: Set up Terraform + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 + + - name: Set up Enos + uses: hashicorp/action-setup-enos@v1 + with: + github-token: ${{ secrets.SERVICE_USER_GITHUB_TOKEN }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_CI }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_CI }} + aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_ROLE_ARN_CI }} + role-skip-session-tagging: true + role-duration-seconds: 3600 + + - name: Install Vault CLI + run: | + wget https://releases.hashicorp.com/vault/1.17.6/vault_1.17.6_linux_amd64.zip -O /tmp/vault.zip + unzip -o -q /tmp/vault.zip -d "/usr/local/bin" + + - name: Download Boundary Linux AMD64 bundle + id: download + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.1 + with: + name: ${{ inputs.artifact-name }} + path: ./enos/support/downloads + + - name: Unzip and rename Boundary bundle + run: | + unzip -o -q ${{steps.download.outputs.download-path}}/*.zip -d "/usr/local/bin" + mv ${{steps.download.outputs.download-path}}/*.zip enos/support/boundary.zip + + - name: Download Boundary Linux AMD64 docker image + if: contains(matrix.filter, 'docker') + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + id: download-docker + with: + name: ${{ inputs.docker-image-file }} + path: ./enos/support/downloads + + - name: Rename docker image file + if: contains(matrix.filter, 'docker') + run: | + mv ${{ steps.download-docker.outputs.download-path }}/*.tar enos/support/boundary_docker_image.tar + + - name: GPG setup + run: | + # Create a GPG key + KEY_PW=boundary + export KEY_PW + gpg --generate-key --batch <> ~/.gnupg/gpg-agent.conf + gpg-connect-agent reloadagent /bye &>/dev/null + + # Get information about the created keys + lines="$(gpg --list-secret-keys --with-colons --with-keygrip)" + KEY_ID="" + while read -r line + do + # Save the first key id to be used later + if [[ $line =~ "fpr"* ]]; then + if [[ $KEY_ID == "" ]]; then + KEY_ID="$(echo "$line" | sed -r 's/fpr|://g')" + fi + fi + + # Cache the passphrases for the keys so passwords do not need to be entered + if [[ $line =~ "grp"* ]]; then + KEYGRIP_ID="$(echo "$line" | sed -r 's/grp|://g')" + /usr/lib/gnupg/gpg-preset-passphrase --preset -P "$KEY_PW" "$KEYGRIP_ID" + fi + done <<< "$lines" + + # Trust the key + touch /tmp/test.txt + gpg -a --encrypt -r "$KEY_ID" --trust-model always --batch --yes /tmp/test.txt + echo "trusted-key $KEY_ID" >> ~/.gnupg/gpg.conf + + # Initialize the password store + sudo apt-get update + sudo apt-get install -y pass + pass init "$KEY_ID" &>/dev/null + + - name: Prepare scenario dependencies + id: prepare_scenario + run: | + mkdir -p ./enos/support + echo "${{ secrets.SSH_KEY_PRIVATE_CI }}" > ./enos/support/private_key.pem + chmod 600 ./enos/support/private_key.pem + + - name: Set up test infra + id: infra + run: | + export ENOS_VAR_enos_user=$GITHUB_ACTOR && \ + export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \ + enos scenario launch --timeout 60m0s --chdir ./enos ${{matrix.filter}} + + - name: Get SHA of UI version + id: get-ui-version + run: | + echo "Using UI Version $(cat ./internal/ui/VERSION)" + echo "sha=$(head -n 1 ./internal/ui/VERSION)" >> "$GITHUB_OUTPUT" + echo "repo=boundary-ui" >> "$GITHUB_OUTPUT" + + - name: Checkout UI repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: hashicorp/${{ steps.get-ui-version.outputs.repo }} + ref: ${{ steps.get-ui-version.outputs.sha }} + path: support/src/ui + token: ${{ secrets.SERVICE_USER_GITHUB_TOKEN }} + + - name: Set up pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + run_install: false + package_json_file: ./support/src/ui/package.json + + - name: Install node dependencies + run: | + cd ./support/src/ui + pnpm install --frozen-lockfile + + cd e2e-tests + pnpm playwright install --with-deps + + - name: GH fix for localhost resolution + run: | + cat /etc/hosts && echo "-----------" + sudo sed -i 's/::1 *localhost ip6-localhost ip6-loopback/::1 ip6 -localhost ip6-loopback/g' /etc/hosts + cat /etc/hosts + ssh -V + + - name: Run Tests + id: run-tests + run: | + # shellcheck disable=SC1090 + source <(bash ./enos/scripts/test_e2e_env.sh) + + cd ./support/src/ui/e2e-tests + pnpm run admin:ce:${{matrix.infra}} --reporter=html + + - name: Split scenario name + if: ${{ failure() && steps.run-tests.outcome == 'failure' }} + id: split + run: | + SCENARIO=$(echo "${{ matrix.filter }}" | cut -d' ' -f1,3 | sed 's/:/_/g') + echo fragment="${SCENARIO}" >> "$GITHUB_OUTPUT" + + - name: Upload Playwright report + if: ${{ failure() && steps.run-tests.outcome == 'failure' }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: playwright-report-${{ steps.split.outputs.fragment }} + path: ./support/src/ui/e2e-tests/playwright-report + + - name: Clean up test infra + if: ${{ always() }} + continue-on-error: true + run: | + export ENOS_VAR_enos_user=$GITHUB_ACTOR && \ + enos scenario destroy --timeout 60m0s --chdir ./enos ${{matrix.filter}} + + - name: Send Slack message on failure + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 + if: failure() && github.repository == 'hashicorp/boundary' + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOUNDARY_TEST_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_BOUNDARY_TEST_BOT_CHANNEL_ID }} + text: ":x: admin ui tests failed (${{ matrix.filter }}): ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref || github.ref_name }}" diff --git a/enos/enos-scenario-e2e-ui-aws.hcl b/enos/enos-scenario-e2e-ui-aws.hcl index dd9998bbb3..b9538d3a51 100644 --- a/enos/enos-scenario-e2e-ui-aws.hcl +++ b/enos/enos-scenario-e2e-ui-aws.hcl @@ -15,11 +15,9 @@ scenario "e2e_ui_aws" { } locals { - aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) - boundary_install_dir = abspath(var.boundary_install_dir) - license_path = abspath(var.boundary_license_path != null ? var.boundary_license_path : joinpath(path.root, "./support/boundary.hclic")) - local_boundary_dir = var.local_boundary_dir != null ? abspath(var.local_boundary_dir) : null - local_boundary_ui_src_dir = var.local_boundary_ui_src_dir != null ? abspath(var.local_boundary_ui_src_dir) : null + aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) + boundary_install_dir = abspath(var.boundary_install_dir) + license_path = abspath(var.boundary_license_path != null ? var.boundary_license_path : joinpath(path.root, "./support/boundary.hclic")) build_path = { "local" = "/tmp", "crt" = var.crt_bundle_path == null ? null : abspath(var.crt_bundle_path) @@ -194,27 +192,24 @@ scenario "e2e_ui_aws" { ] variables { - debug_no_run = var.e2e_debug_no_run - alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr - auth_method_id = step.create_boundary_cluster.auth_method_id - auth_login_name = step.create_boundary_cluster.auth_login_name - auth_password = step.create_boundary_cluster.auth_password - local_boundary_dir = local.local_boundary_dir - local_boundary_ui_src_dir = local.local_boundary_ui_src_dir - aws_ssh_private_key_path = local.aws_ssh_private_key_path - target_address = step.create_targets_with_tag.target_private_ips[0] - target_user = "ubuntu" - target_port = "22" - vault_addr_public = step.create_vault_cluster.instance_addresses[0] - vault_addr_private = step.create_vault_cluster.instance_addresses_private[0] - vault_root_token = step.create_vault_cluster.vault_root_token - aws_access_key_id = step.iam_setup.access_key_id - aws_secret_access_key = step.iam_setup.secret_access_key - aws_host_set_filter = step.create_tag_inputs.tag_string - aws_host_set_ips = step.create_targets_with_tag.target_private_ips - worker_tag_egress = local.egress_tag - aws_region = var.aws_region - alb_cert = matrix.protocol == "https" ? step.create_boundary_cluster.alb_cert : "" + alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr + auth_method_id = step.create_boundary_cluster.auth_method_id + auth_login_name = step.create_boundary_cluster.auth_login_name + auth_password = step.create_boundary_cluster.auth_password + aws_ssh_private_key_path = local.aws_ssh_private_key_path + target_address = step.create_targets_with_tag.target_private_ips[0] + target_user = "ubuntu" + target_port = "22" + vault_addr_public = step.create_vault_cluster.instance_addresses[0] + vault_addr_private = step.create_vault_cluster.instance_addresses_private[0] + vault_root_token = step.create_vault_cluster.vault_root_token + aws_access_key_id = step.iam_setup.access_key_id + aws_secret_access_key = step.iam_setup.secret_access_key + aws_host_set_filter = step.create_tag_inputs.tag_string + aws_host_set_ips = step.create_targets_with_tag.target_private_ips + worker_tag_egress = local.egress_tag + aws_region = var.aws_region + alb_cert = matrix.protocol == "https" ? step.create_boundary_cluster.alb_cert : "" } } diff --git a/enos/enos-scenario-e2e-ui-docker.hcl b/enos/enos-scenario-e2e-ui-docker.hcl index dc8e182146..3809039014 100644 --- a/enos/enos-scenario-e2e-ui-docker.hcl +++ b/enos/enos-scenario-e2e-ui-docker.hcl @@ -14,8 +14,6 @@ scenario "e2e_ui_docker" { locals { aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) - local_boundary_dir = var.local_boundary_dir != null ? abspath(var.local_boundary_dir) : null - local_boundary_ui_src_dir = var.local_boundary_ui_src_dir != null ? abspath(var.local_boundary_ui_src_dir) : null boundary_docker_image_file = abspath(var.boundary_docker_image_file) license_path = abspath(var.boundary_license_path != null ? var.boundary_license_path : joinpath(path.root, "./support/boundary.hclic")) @@ -154,32 +152,29 @@ scenario "e2e_ui_docker" { step.create_ldap_server, ] variables { - debug_no_run = var.e2e_debug_no_run - alb_boundary_api_addr = step.create_boundary.address - auth_method_id = step.create_boundary.auth_method_id - auth_login_name = step.create_boundary.login_name - auth_password = step.create_boundary.password - local_boundary_dir = local.local_boundary_dir - local_boundary_ui_src_dir = local.local_boundary_ui_src_dir - aws_ssh_private_key_path = local.aws_ssh_private_key_path - target_address = step.create_host.address - target_port = step.create_host.port - target_user = "ubuntu" - target_ca_key = step.create_host.ca_key_private - target_ca_key_public = step.create_host.ca_key_public - vault_addr_public = step.create_vault.address_public - vault_addr_private = step.create_vault.address_private - vault_addr_unified = step.create_vault.address_unified - vault_root_token = step.create_vault.token - vault_port = step.create_vault.port - ldap_address = step.create_ldap_server.address - ldap_domain_dn = step.create_ldap_server.domain_dn - ldap_admin_dn = step.create_ldap_server.admin_dn - ldap_admin_password = step.create_ldap_server.admin_password - ldap_user_name = step.create_ldap_server.user_name - ldap_user_password = step.create_ldap_server.user_password - ldap_group_name = step.create_ldap_server.group_name - worker_tag_egress = local.egress_tag + alb_boundary_api_addr = step.create_boundary.address + auth_method_id = step.create_boundary.auth_method_id + auth_login_name = step.create_boundary.login_name + auth_password = step.create_boundary.password + aws_ssh_private_key_path = local.aws_ssh_private_key_path + target_address = step.create_host.address + target_port = step.create_host.port + target_user = "ubuntu" + target_ca_key = step.create_host.ca_key_private + target_ca_key_public = step.create_host.ca_key_public + vault_addr_public = step.create_vault.address_public + vault_addr_private = step.create_vault.address_private + vault_addr_unified = step.create_vault.address_unified + vault_root_token = step.create_vault.token + vault_port = step.create_vault.port + ldap_address = step.create_ldap_server.address + ldap_domain_dn = step.create_ldap_server.domain_dn + ldap_admin_dn = step.create_ldap_server.admin_dn + ldap_admin_password = step.create_ldap_server.admin_password + ldap_user_name = step.create_ldap_server.user_name + ldap_user_password = step.create_ldap_server.user_password + ldap_group_name = step.create_ldap_server.group_name + worker_tag_egress = local.egress_tag } } } diff --git a/enos/modules/test_e2e_ui/main.tf b/enos/modules/test_e2e_ui/main.tf index 7079de8bea..f7f5b316ff 100644 --- a/enos/modules/test_e2e_ui/main.tf +++ b/enos/modules/test_e2e_ui/main.tf @@ -9,11 +9,6 @@ terraform { } } -variable "debug_no_run" { - description = "If set, this module will not execute the tests so that you can still access environment variables" - type = bool - default = true -} variable "alb_boundary_api_addr" { description = "URL of the Boundary instance" type = string @@ -34,15 +29,6 @@ variable "auth_password" { type = string default = "" } -variable "local_boundary_dir" { - description = "Local Path to boundary executable" - type = string -} -variable "local_boundary_ui_src_dir" { - description = "Local Path to boundary-ui directory" - type = string -} - variable "aws_ssh_private_key_path" { description = "Local Path to key used to SSH onto created hosts" type = string @@ -250,7 +236,7 @@ resource "enos_local_exec" "run_e2e_test" { E2E_ALB_CERT = var.alb_cert } - inline = var.debug_no_run ? [""] : ["set -o pipefail; PATH=\"${var.local_boundary_dir}:$PATH\" pnpm --cwd ${var.local_boundary_ui_src_dir}/ui/admin run e2e 2>&1 | tee ${path.module}/../../test-e2e-ui.log"] + inline = [""] } output "test_results" { diff --git a/internal/ui/VERSION b/internal/ui/VERSION index 9a3451ad33..bced94f303 100644 --- a/internal/ui/VERSION +++ b/internal/ui/VERSION @@ -1,4 +1,4 @@ -2766be2c2e941020878f837af6188eb500579e31 +28b185bec89e021e8d660b5d3095fcd114955431 # This file determines the version of the UI to embed in the boundary binary. # Update this file by running 'make update-ui-version' from the root of this repo. # Set UI_COMMITISH when running the above target to update to a specific version.