diff --git a/.github/scripts/delete-aws-resources.sh b/.github/scripts/delete-aws-resources.sh new file mode 100755 index 00000000..37a51ff7 --- /dev/null +++ b/.github/scripts/delete-aws-resources.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Copyright 2023 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + + +APP_NAME=$1 +APP_LABEL='RadiusApplication' +RESOURCE_TYPES='AWS::RDS::DBInstance,AWS::RDS::DBSubnetGroup,AWS::MemoryDB::Cluster,AWS::MemoryDB::SubnetGroup' + +# File to store the list of deleted resources +DELETED_RESOURCES_FILE='deleted-resources.txt' + +# Number of retries +MAX_RETRIES=5 + +# Retry delay in seconds +RETRY_DELAY=300 # 5 minutes + +function delete_aws_resources() { + # Empty the file + truncate -s 0 $DELETED_RESOURCES_FILE + + for resource_type in ${RESOURCE_TYPES//,/ } + do + aws cloudcontrol list-resources --type-name "$resource_type" --query "ResourceDescriptions[].Identifier" --output text | tr '\t' '\n' | while read identifier + do + aws cloudcontrol get-resource --type-name "$resource_type" --identifier "$identifier" --query "ResourceDescription.Properties" --output text | while read resource + do + resource_tags=$(jq -c -r .Tags <<< "$resource") + for tag in $(jq -c -r '.[]' <<< "$resource_tags") + do + key=$(jq -r '.Key' <<< "$tag") + value=$(jq -r '.Value' <<< "$tag") + if [[ "$key" == "$APP_LABEL" && "$value" == "$APP_NAME" ]] + then + echo "Deleting resource of type: $resource_type with identifier: $identifier" + echo "$identifier\n" >> $DELETED_RESOURCES_FILE + aws cloudcontrol delete-resource --type-name "$resource_type" --identifier "$identifier" + fi + done + done + done + done + + if [ -s $DELETED_RESOURCES_FILE ]; then + return 1 + else + return 0 + fi +} + +RETRY_COUNT=0 +while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + # Trigger the function to delete the resources + delete_aws_resources + + # If the function returned 0, then no resources needed to be deleted + # on this run. This means that all resources have been deleted. + if [ $? -eq 0 ]; then + echo "All resources deleted successfully" + break + fi + + # Still have resources to delete, increase the retry count + RETRY_COUNT=$((RETRY_COUNT + 1)) + + # Check if there are more retries left + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + # Retry after delay + echo "Retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi +done + +# Check if the maximum number of retries exceeded +if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo "Maximum number of retries exceeded" +fi diff --git a/.github/workflows/purge-test-resources.yaml b/.github/workflows/purge-azure-test-resources.yaml similarity index 50% rename from .github/workflows/purge-test-resources.yaml rename to .github/workflows/purge-azure-test-resources.yaml index db47b568..91bc0bd3 100644 --- a/.github/workflows/purge-test-resources.yaml +++ b/.github/workflows/purge-azure-test-resources.yaml @@ -1,4 +1,4 @@ -name: Purge test resources +name: Purge Azure test resources on: schedule: - cron: "30 0,12 * * *" @@ -7,7 +7,7 @@ env: VALID_RESOURCE_WINDOW: 6*60*60 jobs: purge_azure_resources: - name: Azure resources clean-ups + name: Azure test resource cleanup runs-on: [self-hosted, 1ES.Pool=1ES-Radius] steps: - name: Login to Azure @@ -18,30 +18,47 @@ jobs: --tenant ${{ secrets.AZURE_SP_TESTS_TENANTID }} az account set --subscription ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} - - name: List Test Resource Groups run: | echo "## Test resource group list" >> $GITHUB_STEP_SUMMARY - az group list --query "[?starts_with(name, 'samplestest-')].{Name:name, creationTime:tags.creationTime}" -o json > resource_groups.json + + # Create the file to store the resource group list + touch ${{ env.AZURE_RG_DELETE_LIST_FILE}} + + resource_groups=$(az group list --query "[].{Name:name, creationTime:tags.creationTime}" -o tsv) current_time=$(date +%s) hours_ago=$((current_time - ${{ env.VALID_RESOURCE_WINDOW }})) + while IFS=$'\t' read -r name creation_time; do + if [[ ! "$name" =~ ^"samplestest-" ]]; then + continue + fi - jq -r '.[] | select(.creationTime == null || .creationTime < '$hours_ago') | .Name' resource_groups.json > ${{ env.AZURE_RG_DELETE_LIST_FILE}} - jq -r '.[] | {name: .Name, creationTime: .creationTime // "None"}' resource_groups.json > $GITHUB_STEP_SUMMARY + if [ "$creation_time" = "None" ]; then + echo " * :wastebasket: $name - old resource" >> $GITHUB_STEP_SUMMARY + echo $name >> ${{ env.AZURE_RG_DELETE_LIST_FILE}} + continue + fi + # Check if the resource group was created more than 6 hours ago + if [ "$creation_time" -lt "$hours_ago" ]; then + echo " * :wastebasket: $name - creationTime: $creation_time" >> $GITHUB_STEP_SUMMARY + echo $name >> ${{ env.AZURE_RG_DELETE_LIST_FILE}} + else + echo " * :white_check_mark: $name - creationTime: $creation_time" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$resource_groups" - name: Delete Azure Resource Groups run: | echo "## Deleting resource group list" >> $GITHUB_STEP_SUMMARY - cat ${{ env.AZURE_RG_DELETE_LIST_FILE}} | while read -r line + cat ${{ env.AZURE_RG_DELETE_LIST_FILE}} | while read line do echo " * $line" >> $GITHUB_STEP_SUMMARY az group delete --resource-group $line --yes --verbose done - - name: Create GitHub issue on failure if: ${{ failure() }} run: | - gh issue create --title "Samples purge test resources failed \ + gh issue create --title "Samples purge Azure test resources failed" \ --body "Test failed on ${{ github.repository }}. See [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details." \ - s--repo ${{ github.repository }} + --repo ${{ github.repository }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ff2489ac..83ba3627 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Radius version number to use (e.g. 0.22.0, 0.23.0-rc1)' + description: 'Radius version number to use (e.g. 0.1.0, 0.1.0-rc1)' required: true default: '' type: string diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 456790f8..71209980 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Radius version number to use (e.g. 0.22.0, 0.23.0-rc1, edge). Defaults to edge.' + description: 'Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge.' required: false default: 'edge' type: string @@ -26,6 +26,7 @@ on: - cron: "45 15 * * *" env: RAD_CLI_URL: https://get.radapp.dev/tools/rad/install.sh + RUN_IDENTIFIER: samplestest-${{ github.run_id }}-${{ github.run_attempt }} jobs: test: name: Sample tests @@ -39,7 +40,7 @@ jobs: app: demo path: ./demo/app.bicep args: --application demo - uiTestFile: tests/demo.app.spec.ts + uiTestFile: tests/demo/demo.app.spec.ts port: 3000 container: demo enableDapr: false @@ -62,29 +63,37 @@ jobs: runOnPullRequest: true app: eshop path: ./reference-apps/eshop/iac/eshop.bicep - args: --application eshop uiTestFile: tests/eshop/container.app.spec.ts enableDapr: false - name: eshop-azure runOnPullRequest: false - app: eshop + app: eshop-azure path: ./reference-apps/eshop/iac/eshop.bicep - args: --application eshop -p platform=azure + args: -p platform=azure -p appName=eshop-azure uiTestFile: tests/eshop/container.app.spec.ts credential: azure enableDapr: false + - name: eshop-aws + runOnPullRequest: false + app: eshop-aws-${{ github.run_id }}-${{ github.run_attempt }} + path: ./reference-apps/eshop/iac/eshop.bicep + args: -p platform=aws -p eksClusterName=eks-samplestest-${{ github.run_id }}-${{ github.run_attempt }}-eshop-aws -p appName=eshop-aws-${{ github.run_id }}-${{ github.run_attempt }} + uiTestFile: tests/eshop/container.app.spec.ts + credential: aws + enableDapr: false env: BRANCH: ${{ github.base_ref || github.ref_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} AZURE_LOCATION: westus3 + AWS_REGION: us-west-2 + AWS_ZONES: us-west-2a,us-west-2b,us-west-2c steps: # Setup the test assets and configuration - name: Generate output variables id: gen-id run: | - BASE_STR="SAMPLES|${GITHUB_SHA}|${GITHUB_SERVER_URL}|${GITHUB_REPOSITORY}|${GITHUB_RUN_ID}|${GITHUB_RUN_ATTEMPT}" - UNIQUE_ID=$(echo $BASE_STR | sha1sum | head -c 10) - + RUN_IDENTIFIER=${{ env.RUN_IDENTIFIER }}-${{ matrix.name }} + if [[ "${{ github.event_name }}" == "pull_request" && "${{ matrix.runOnPullRequest }}" == "false" ]]; then RUN_TEST=false else @@ -98,15 +107,16 @@ jobs: fi # Set output variables to be used in the other jobs - echo "UNIQUE_ID=${UNIQUE_ID}" >> $GITHUB_OUTPUT - echo "TEST_RESOURCE_GROUP_PREFIX=samplestest-${UNIQUE_ID}" >> $GITHUB_OUTPUT + echo "RUN_IDENTIFIER=${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT + echo "TEST_AZURE_RESOURCE_GROUP=rg-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT + echo "TEST_EKS_CLUSTER_NAME=eks-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT echo "RUN_TEST=${RUN_TEST}" >> $GITHUB_OUTPUT echo "ENABLE_DAPR=${ENABLE_DAPR}" >> $GITHUB_OUTPUT - name: Checkout code if: steps.gen-id.outputs.RUN_TEST == 'true' uses: actions/checkout@v3 - name: Ensure inputs.version is valid semver - if: inputs.version != '' && steps.gen-id.outputs.RUN_TEST == 'true' + if: steps.gen-id.outputs.RUN_TEST == 'true' && inputs.version != '' run: | python ./.github/scripts/validate_semver.py ${{ inputs.version }} - name: Setup Node @@ -115,7 +125,7 @@ jobs: with: node-version: 16 - name: az CLI login - if: matrix.credential == 'azure' && steps.gen-id.outputs.RUN_TEST == 'true' + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure' run: | az login --service-principal \ --username ${{ secrets.AZURE_SP_TESTS_APPID }} \ @@ -123,25 +133,59 @@ jobs: --tenant ${{ secrets.AZURE_SP_TESTS_TENANTID }} # Create and install test environment - name: Create Azure resource group + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure' id: create-azure-resource-group - if: matrix.credential == 'azure' && steps.gen-id.outputs.RUN_TEST == 'true' - env: - RESOURCE_GROUP: ${{ steps.gen-id.outputs.TEST_RESOURCE_GROUP_PREFIX }}-${{ matrix.name }} - SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} run: | current_time=$(date +%s) az group create \ --location ${{ env.AZURE_LOCATION }} \ - --name $RESOURCE_GROUP \ - --subscription $SUBSCRIPTION_ID \ + --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \ + --subscription ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} \ --tags creationTime=$current_time - while [ $(az group exists --name $RESOURCE_GROUP --subscription $SUBSCRIPTION_ID) = false ]; do - echo "Waiting for resource group $RESOURCE_GROUP to be created..." + while [ $(az group exists --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} --subscription ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}) = false ]; do + echo "Waiting for resource group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} to be created..." sleep 5 done + - name: Configure AWS + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} + aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws configure set region ${{ env.AWS_REGION }} + aws configure set output json + - name: Create EKS Cluster + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + id: create-eks + run: | + curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp + sudo mv /tmp/eksctl /usr/local/bin + eksctl create cluster \ + --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} \ + --nodes-min 1 --nodes-max 2 --node-type t3.large \ + --zones ${{ env.AWS_ZONES }} \ + --managed \ + --region ${{ env.AWS_REGION }} + while [[ "$(eksctl get cluster ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} -o json | jq -r .[0].Status)" != "ACTIVE" ]]; do + echo "Waiting for EKS cluster to be created..." + sleep 60 + done + timeout-minutes: 60 + continue-on-error: false + - name: Install k3d + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} - name: Download k3d - if: steps.gen-id.outputs.RUN_TEST == 'true' + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential != 'aws' run: wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + - name: Create k3d cluster + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential != 'aws' + run: k3d cluster create --agents 2 -p "80:80@loadbalancer" --k3s-arg "--disable=traefik@server:0" + - name: Install Dapr + if: steps.gen-id.outputs.RUN_TEST == 'true' && steps.gen-id.outputs.ENABLE_DAPR == 'true' + run: | + helm repo add dapr https://dapr.github.io/helm-charts/ + helm install dapr dapr/dapr --version=1.6 --namespace dapr-system --create-namespace --wait - name: Download rad CLI if: steps.gen-id.outputs.RUN_TEST == 'true' run: | @@ -162,21 +206,14 @@ jobs: break fi done - - name: Create k3d cluster - if: steps.gen-id.outputs.RUN_TEST == 'true' - run: k3d cluster create --agents 2 -p "80:80@loadbalancer" --k3s-arg "--disable=traefik@server:0" - - name: Install Dapr - if: steps.gen-id.outputs.RUN_TEST == 'true' && steps.gen-id.outputs.ENABLE_DAPR == 'true' - run: | - helm repo add dapr https://dapr.github.io/helm-charts/ - helm install dapr dapr/dapr --version=1.6 --namespace dapr-system --create-namespace --wait - - name: Init local environment + - name: Initialize local environment if: steps.gen-id.outputs.RUN_TEST == 'true' - env: - RESOURCE_GROUP: ${{ steps.gen-id.outputs.TEST_RESOURCE_GROUP_PREFIX }}-${{ matrix.name }} - SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} run: | - rad install kubernetes --set rp.publicEndpointOverride=localhost + if [[ "${{ matrix.credential }}" == "aws" ]]; then + rad install kubernetes + else + rad install kubernetes --set rp.publicEndpointOverride=localhost + fi rad group create default rad workspace create kubernetes default --group default rad group switch default @@ -185,9 +222,13 @@ jobs: rad recipe register default -e default -w default --template-kind bicep --template-path radius.azurecr.io/recipes/dev/rediscaches:latest --resource-type Applications.Datastores/redisCaches rad recipe register default -e default -w default --template-kind bicep --template-path radius.azurecr.io/recipes/dev/mongodatabases:latest --resource-type Applications.Datastores/mongoDatabases if [[ "${{ matrix.credential }}" == "azure" ]]; then - rad env update default --azure-subscription-id $SUBSCRIPTION_ID --azure-resource-group $RESOURCE_GROUP + rad env update default --azure-subscription-id ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} --azure-resource-group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} rad credential register azure --client-id ${{ secrets.AZURE_SP_TESTS_APPID }} --client-secret ${{ secrets.AZURE_SP_TESTS_PASSWORD }} --tenant-id ${{ secrets.AZURE_SP_TESTS_TENANTID }} fi + if [[ "${{ matrix.credential }}" == "aws" ]]; then + rad env update default --aws-region ${{ env.AWS_REGION }} --aws-account-id ${{ secrets.AWS_ACCOUNT_ID }} + rad credential register aws --access-key-id ${{ secrets.AWS_ACCESS_KEY_ID }} --secret-access-key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + fi # Deploy application and run tests - name: Deploy app if: steps.gen-id.outputs.RUN_TEST == 'true' @@ -199,16 +240,20 @@ jobs: label="radius.dev/application=${{ matrix.app }}" kubectl wait --for=condition=Ready pod -l $label -n $namespace --timeout=5m - name: Run Playwright Test + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.uiTestFile != '' id: run-playwright-test - if: matrix.uiTestFile != '' && steps.gen-id.outputs.RUN_TEST == 'true' run: | if [[ "${{ matrix.container }}" != "" ]]; then rad resource expose containers ${{ matrix.container }} ${{ matrix.args }} --port ${{ matrix.port }} & + else + endpoint="$(rad app status -a ${{ matrix.app }} | sed 's/ /\n/g' | grep http)" + echo "Endpoint: $endpoint" + export ENDPOINT=$endpoint fi cd ui-tests/ npm ci npx playwright install --with-deps - npx playwright test ${{ matrix.uiTestFile }} --retries=3 + npx playwright test ${{ matrix.uiTestFile }} --retries 3 - name: Upload Playwright Results uses: actions/upload-artifact@v3 if: always() && ( steps.run-playwright-test.outcome == 'success' || steps.run-playwright-test.outcome == 'failure' ) @@ -248,15 +293,27 @@ jobs: # Cleanup - name: Delete app if: steps.gen-id.outputs.RUN_TEST == 'true' - run: rad app delete ${{ matrix.app }} -y + run: | + rad app delete ${{ matrix.app }} -y - name: Delete Azure resource group - if: always() && steps.create-azure-resource-group.outcome == 'success' - env: - RESOURCE_GROUP: ${{ steps.gen-id.outputs.TEST_RESOURCE_GROUP_PREFIX }}-${{ matrix.name }} - SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && steps.create-azure-resource-group.outcome == 'success' run: | - # if deletion fails, purge workflow will purge the resource group and its resources later. + # Delete Azure resources created by the test + # if deletion fails, purge workflow will purge the resource group and its resources later az group delete \ - --subscription $SUBSCRIPTION_ID \ - --name $RESOURCE_GROUP \ + --subscription ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} \ + --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \ --yes + - name: Delete AWS Resources + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + # Delete all AWS resources created by the test + ./.github/scripts/delete-aws-resources.sh ${{ matrix.app }} + - name: Delete EKS Cluster + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + # Uninstall Radius from EKS cluster + rad uninstall kubernetes + # Delete EKS cluster + echo "Deleting EKS cluster: ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }}" + eksctl delete cluster --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} --wait diff --git a/reference-apps/eshop/iac/eshop.bicep b/reference-apps/eshop/iac/eshop.bicep index fcac2d6b..2f6b0308 100644 --- a/reference-apps/eshop/iac/eshop.bicep +++ b/reference-apps/eshop/iac/eshop.bicep @@ -94,6 +94,7 @@ module aws 'infra/aws.bicep' = if (platform == 'aws') { environment: environment adminLogin: adminLogin adminPassword: adminPassword + applicationName: appName } } diff --git a/reference-apps/eshop/iac/infra/aws.bicep b/reference-apps/eshop/iac/infra/aws.bicep index 8883310d..23180522 100644 --- a/reference-apps/eshop/iac/infra/aws.bicep +++ b/reference-apps/eshop/iac/infra/aws.bicep @@ -7,6 +7,9 @@ param environment string @description('Radius application ID') param application string +@description('Radius application name') +param applicationName string + @description('SQL administrator username') param adminLogin string @@ -33,6 +36,12 @@ resource sqlSubnetGroup 'AWS.RDS/DBSubnetGroup@default' = { DBSubnetGroupName: sqlSubnetGroupName DBSubnetGroupDescription: sqlSubnetGroupName SubnetIds: eksCluster.properties.ResourcesVpcConfig.SubnetIds + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -56,6 +65,12 @@ resource identityDb 'AWS.RDS/DBInstance@default' = { LicenseModel: 'license-included' Timezone: 'GMT Standard Time' CharacterSetName: 'Latin1_General_CI_AS' + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -79,6 +94,12 @@ resource catalogDb 'AWS.RDS/DBInstance@default' = { LicenseModel: 'license-included' Timezone: 'GMT Standard Time' CharacterSetName: 'Latin1_General_CI_AS' + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -102,6 +123,12 @@ resource orderingDb 'AWS.RDS/DBInstance@default' = { LicenseModel: 'license-included' Timezone: 'GMT Standard Time' CharacterSetName: 'Latin1_General_CI_AS' + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -125,6 +152,12 @@ resource webhooksDb 'AWS.RDS/DBInstance@default' = { LicenseModel: 'license-included' Timezone: 'GMT Standard Time' CharacterSetName: 'Latin1_General_CI_AS' + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -134,6 +167,12 @@ resource redisSubnetGroup 'AWS.MemoryDB/SubnetGroup@default' = { properties: { SubnetGroupName: redisSubnetGroupName SubnetIds: eksCluster.properties.ResourcesVpcConfig.SubnetIds + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -147,6 +186,12 @@ resource keystoreCache 'AWS.MemoryDB/Cluster@default' = { SecurityGroupIds: [eksCluster.properties.ClusterSecurityGroupId] SubnetGroupName: redisSubnetGroup.properties.SubnetGroupName NumReplicasPerShard: 0 + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } @@ -160,6 +205,12 @@ resource basketCache 'AWS.MemoryDB/Cluster@default' = { SecurityGroupIds: [eksCluster.properties.ClusterSecurityGroupId] SubnetGroupName: redisSubnetGroup.name NumReplicasPerShard: 0 + Tags: [ + { + Key: 'RadiusApplication' + Value: applicationName + } + ] } } diff --git a/ui-tests/package-lock.json b/ui-tests/package-lock.json index fb5b6ee4..d1ef7baa 100644 --- a/ui-tests/package-lock.json +++ b/ui-tests/package-lock.json @@ -12,7 +12,10 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@playwright/test": "^1.35.0" + "@playwright/test": "^1.35.0", + "@types/node": "^20.6.0", + "@types/uuid": "^9.0.3", + "typescript": "^5.2.2" } }, "node_modules/@playwright/test": { @@ -35,9 +38,15 @@ } }, "node_modules/@types/node": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.0.tgz", - "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", + "version": "20.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", + "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", + "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==", "dev": true }, "node_modules/fsevents": { @@ -66,6 +75,19 @@ "node": ">=16" } }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", diff --git a/ui-tests/package.json b/ui-tests/package.json index 178df5ea..d3d7f2f1 100644 --- a/ui-tests/package.json +++ b/ui-tests/package.json @@ -8,7 +8,10 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.35.0" + "@playwright/test": "^1.35.0", + "@types/node": "^20.6.0", + "@types/uuid": "^9.0.3", + "typescript": "^5.2.2" }, "dependencies": { "uuid": "^9.0.0" diff --git a/ui-tests/tests/demo.app.spec.ts b/ui-tests/tests/demo/demo.app.spec.ts similarity index 100% rename from ui-tests/tests/demo.app.spec.ts rename to ui-tests/tests/demo/demo.app.spec.ts diff --git a/ui-tests/tests/eshop/container.app.spec.ts b/ui-tests/tests/eshop/container.app.spec.ts index 0da587ed..0b67ab26 100644 --- a/ui-tests/tests/eshop/container.app.spec.ts +++ b/ui-tests/tests/eshop/container.app.spec.ts @@ -8,11 +8,16 @@ test("eShop on Containers App Basic UI and Functionality Checks", async ({ page } }); - // Go to http://localhost - await page.goto("http://localhost"); + let endpoint = process.env.ENDPOINT + expect(endpoint).toBeDefined() + + // Remove quotes from the endpoint if they exist + endpoint = (endpoint as string).replace(/['"]+/g, '') + + await page.goto(endpoint); // Expect page to have proper URL - expect(page).toHaveURL("http://localhost/catalog"); + expect(page).toHaveURL(endpoint+"/catalog"); // Expect page to have proper title expect(page).toHaveTitle("eShopOnContainers - SPA"); @@ -48,7 +53,7 @@ test("eShop on Containers App Basic UI and Functionality Checks", async ({ page // After login, expect to be redirected to the catalog page // Expect page to have proper URL - expect(page).toHaveURL("http://localhost/catalog"); + expect(page).toHaveURL(endpoint+"/catalog"); // Expect page to have proper title expect(page).toHaveTitle("eShopOnContainers - SPA"); @@ -77,7 +82,7 @@ test("eShop on Containers App Basic UI and Functionality Checks", async ({ page .click(); // Expect page to have proper URL - expect(page).toHaveURL("http://localhost/basket"); + expect(page).toHaveURL(endpoint+"/basket"); // Checkout await page.getByRole('button', { name: 'Checkout' }) diff --git a/ui-tests/tsconfig.json b/ui-tests/tsconfig.json new file mode 100644 index 00000000..e075f973 --- /dev/null +++ b/ui-tests/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}