diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index a28bac1e2..f72b66fa0 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -27,11 +27,11 @@ on: version_tag: required: false type: string - default: master # pr-#, nonprod, or vX.Y.Z + default: master namespace: required: false type: string - default: nonprod # pr-#, nonprod, or prod + default: nonprod secrets: AWS_DEFAULT_REGION: required: true @@ -44,7 +44,6 @@ on: NOTIFY_EMAIL: required: true -# Build and Test Workflow jobs: TestAndBuild: runs-on: ubuntu-latest @@ -68,26 +67,28 @@ jobs: - name: Set up Golang CI Tools run: ./scripts/install_ci.sh - - name: Checkout repository - uses: actions/checkout@v3 - - name: Build Go executables run: make build + - name: Create Release Artifacts + run: | + cd bin + zip -r build_artifacts.zip . + cd .. + zip -r bin/terraform_artifacts.zip modules + - name: Publish build artifacts (bin) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: bin path: bin - name: Publish build artifacts (deploy_scripts) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: deploy_scripts path: scripts - # Deployment Workflow - Deploy: needs: [TestAndBuild] runs-on: ubuntu-latest @@ -98,20 +99,38 @@ jobs: - name: Configure namespace run: | + PR_NUMBER="" + if [[ "${{ github.event.pull_request.number }}" != "" ]]; then - NS="github-pr-${{ github.event.pull_request.number }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + fi + + if [[ -z "$PR_NUMBER" && "${{ inputs.version_tag }}" =~ ^pr-[0-9]+$ ]]; then + PR_NUMBER=$(echo "${{ inputs.version_tag }}" | sed 's/pr-//') + fi + + if [[ -z "$PR_NUMBER" && "${{ inputs.namespace }}" =~ ^pr-[0-9]+$ ]]; then + PR_NUMBER=$(echo "${{ inputs.namespace }}" | sed 's/pr-//') + fi + + if [[ -z "$PR_NUMBER" && "${{ github.event_name }}" == "release" ]]; then + PR_NUMBER=$(echo "${{ github.event.release.tag_name }}" | grep -oE 'pr-[0-9]+' | sed 's/pr-//') + if [[ -z "$PR_NUMBER" ]]; then + PR_NUMBER=$(echo "${{ github.event.release.body }}" | grep -oE '(PR-|#)[0-9]+' | head -n 1 | sed 's/PR-\|#//') + fi + fi + + if [[ -n "$PR_NUMBER" && "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + NS="github-pr-${PR_NUMBER}" else NS="cd" fi - echo "Namespace is ${NS}" - echo "namespace=${NS}" >> $GITHUB_ENV - echo "${NS}" > ./namespace.txt - name: Upload namespace file - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: namespace path: namespace.txt @@ -124,12 +143,12 @@ jobs: run: | echo "PR_NUMBER=${{ env.PR_NUMBER }}" echo "NS_BRANCH_OR_TAG=${{ env.NS_BRANCH_OR_TAG }}" + - name: Set up the Go workspace uses: actions/setup-go@v3 with: go-version: ${{ inputs.go_version }} - # check-latest: true - # token: ${{ secrets.GITHUB_TOKEN }} + - name: Download pipeline dependencies run: | set -ex @@ -137,9 +156,7 @@ jobs: echo "${{ runner.tool_cache }}/go/bin" >> $GITHUB_PATH go install github.com/jstemmer/go-junit-report@latest - # Download dce-cli wget -q https://github.com/Optum/dce-cli/releases/download/v0.5.0/dce_linux_amd64.zip - # Validate checksum expected_sha="cb140c743373e28a6c1bd4ba3fe1b81a7431dd538e1ad430fede3c1aff4508db" test $(shasum -a 256 ./dce_linux_amd64.zip | awk '{print $1}') == "${expected_sha}" unzip ./dce_linux_amd64.zip -d ./ @@ -152,19 +169,17 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 NAMESPACE: ${{ env.namespace }} - DCE_NONPROD_HOSTNAME: ${{secrets.DCE_NONPROD_HOSTNAME}} + DCE_NONPROD_HOSTNAME: ${{ secrets.DCE_NONPROD_HOSTNAME }} run: | set -ex - echo "Principal ID is ${NAMESPACE}" - - echo "Configuring the dce-cli" + echo " api: host: ${{ secrets.DCE_NONPROD_HOSTNAME }} basepath: /api region: us-east-1 " > ./dce.yml - + # Check to see if there's an active lease for this PR lease_id=$( ./dce --config=dce.yml leases list \ @@ -173,7 +188,6 @@ jobs: ) if [ ! "${lease_id}" ]; then - echo "No lease exists for ${NAMESPACE}. Creating one..." created_lease=$( ./dce --config=dce.yml leases create \ --principal-id ${NAMESPACE} \ @@ -181,133 +195,168 @@ jobs: --budget-amount 100 --budget-currency USD \ --email noreply@example.com ) - echo "Created lease: ${created_lease}" lease_id=$(echo "${created_lease}" | jq -r .id) fi - echo "Using lease for PrincipalId=${NAMESPACE}, Id=${lease_id}" - echo "Logging into the DCE account" ./dce --config=dce.yml leases login ${lease_id} - - # Save the lease ID to a file, so we can reference it later - # (note that we can't assign variables across jobs in different stages) echo "${lease_id}" > ./lease_id.txt - # Install Terraform + # Install Terraform - name: Install Terraform uses: hashicorp/setup-terraform@v2 with: terraform_version: ${{ inputs.terraform_version }} - # Configure the Terraform backend + # Configure the Terraform backend - name: Configure Terraform Backend run: | lease_id=$(cat lease_id.txt) ./scripts/create-tf-backend.sh ${lease_id} - - # terraform init + # terraform init - name: Terraform Init/Apply env: NAMESPACE: ${{ env.namespace }} - NOTIFY_EMAIL: ${{secrets.NOTIFY_EMAIL }} + NOTIFY_EMAIL: ${{ secrets.NOTIFY_EMAIL }} + REQUIRED_BUCKET_PREFIX: ${{ vars.REQUIRED_BUCKET_PREFIX }} + TF_VAR_global_tags: ${{ vars.TF_VAR_GLOBAL_TAGS }} run: | set -ex cd modules - cat ./backend.tf terraform init -input=false + + export TF_VAR_global_tags='${{ vars.TF_VAR_GLOBAL_TAGS}}' + export TF_VAR_required_bucket_prefix="${TF_VAR_required_bucket_prefix}" terraform plan \ -var="namespace=${NAMESPACE}" \ -var="budget_notification_from_email=${NOTIFY_EMAIL}" \ - -var="reset_nuke_toggle=false" + -var="reset_nuke_toggle=false" \ + -var="required_bucket_prefix=${TF_VAR_required_bucket_prefix}" \ + -var="global_tags=${TF_VAR_global_tags}" terraform apply \ -auto-approve \ -input=false \ -var="namespace=${NAMESPACE}" \ -var="budget_notification_from_email=${NOTIFY_EMAIL}" \ - -var="reset_nuke_toggle=false" - # Build and Deploy Application Code to AWS - # - name: Set up Node.js - # uses: actions/setup-node@v2 - # with: - # node-version: "14" - - # - name: Install dependencies - # run: npm install + -var="reset_nuke_toggle=false" \ + -var="required_bucket_prefix=${REQUIRED_BUCKET_PREFIX}" \ + -var="global_tags=${TF_VAR_global_tags}" - name: Build artifacts + run: ./scripts/build.sh + + - name: DCE Lease Login + env: + AWS_DEFAULT_REGION: us-east-1 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | - echo "Running build script" - ./scripts/build.sh + set -ex + lease_id=$(cat ./lease_id.txt) + ./dce --config=./dce.yml leases login ${lease_id} - - name: Deploy Application Code + - name: Set Terraform Output Variables + env: + TF_LOG: "" + TF_LOG_CORE: "" + TF_LOG_PROVIDER: "" run: | + cd modules + terraform output artifacts_bucket_name > ../artifacts_bucket_raw.txt 2>&1 + terraform output namespace > ../namespace_raw.txt 2>&1 + + cd .. + + ARTIFACTS_BUCKET=$(grep -o '"[^"]*"' artifacts_bucket_raw.txt | sed 's/"//g' | head -n1) + NAMESPACE=$(grep -o '"[^"]*"' namespace_raw.txt | sed 's/"//g' | head -n1) + + if [[ -z "${ARTIFACTS_BUCKET}" ]]; then + ARTIFACTS_BUCKET=$(grep -v '^\s*$' artifacts_bucket_raw.txt | tail -n1 | sed 's/"//g' | tr -d '\n\r ') + fi + + if [[ -z "${NAMESPACE}" ]]; then + NAMESPACE=$(grep -v '^\s*$' namespace_raw.txt | tail -n1 | sed 's/"//g' | tr -d '\n\r ') + fi + + rm -f artifacts_bucket_raw.txt namespace_raw.txt + + if [[ -z "${ARTIFACTS_BUCKET}" || "${ARTIFACTS_BUCKET}" =~ ^\[command\] ]]; then + echo "Error: ARTIFACTS_BUCKET is empty or contains command text" + exit 1 + fi - echo "Running terraform output for artifacts_bucket_name" - ARTIFACTS_BUCKET=$(cd modules && terraform output artifacts_bucket_name | tr -d '"') # Remove quotes and newlines - echo "ARTIFACTS_BUCKET=${ARTIFACTS_BUCKET}" + if [[ -z "${NAMESPACE}" || "${NAMESPACE}" =~ ^\[command\] ]]; then + echo "Error: NAMESPACE is empty or contains command text" + exit 1 + fi + + if [[ ! "${ARTIFACTS_BUCKET}" =~ ^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]$ ]]; then + echo "Error: ARTIFACTS_BUCKET has invalid format: ${ARTIFACTS_BUCKET}" + exit 1 + fi - echo "Starting to print the artifacts bucket name" - echo "ARTIFACTS_BUCKET=${ARTIFACTS_BUCKET}" + if [[ ! "${NAMESPACE}" =~ ^[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]$ ]]; then + echo "Error: NAMESPACE has invalid format: ${NAMESPACE}" + exit 1 + fi - echo "Running terraform output for namespace" - NAMESPACE=$(cd modules && terraform output namespace) - echo "Output namespace: ${NAMESPACE}" + echo "ARTIFACTS_BUCKET=${ARTIFACTS_BUCKET}" >> $GITHUB_ENV + echo "NAMESPACE=${NAMESPACE}" >> $GITHUB_ENV - ./scripts/deploy.sh \ + - name: Deploy Application Code + run: | + aws sts get-caller-identity + + ./scripts/deploy.sh \ /home/runner/work/dce/dce/bin/build_artifacts.zip \ - github-pr-513\ - 000879607493-dce-artifacts-github-pr-513 + "${NAMESPACE}" \ + "${ARTIFACTS_BUCKET}" # Functional Tests --> TODO need to fix the test failures + # - name: Functional Tests # run: | # set -euxo pipefail # mkdir -p junit-report - # # Run functional tests + # go get github.com/jstemmer/go-junit-report # go test -v ./tests/... -test.timeout 50m 2>&1 | tee >(go-junit-report > junit-report/functional.xml) - - # Publish junit test results (for unit and functional tests) -- > TODO need to fix the test failures + + # Publish junit test results (for unit and functional tests) -- > TODO need to fix the test failures # - name: Publish Test Results # if: always() - # uses: actions/upload-artifact@v4 - # with: - # name: Functional Tests - # path: ${{ github.workspace }}/junit-report/*.xml - - # Publish the dce-cli executable, so we can use - # it in our Cleanup stage + # uses: actions/upload-artifact@v4 + # with: + # name: Functional-Test-Results + # path: junit-report/functional.xml + # retention-days: 7 - name: Upload dce-cli Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: dce-cli path: ${{ github.workspace }}/dce - name: Upload dce-yml Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: dce-yml path: ${{ github.workspace }}/dce.yml - name: Upload lease_id Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: lease_id path: ${{ github.workspace }}/lease_id.txt - name: Upload backend-tf Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 with: name: backend-tf path: ${{ github.workspace }}/modules/backend.tf - - name: Checkout the git repo code - uses: actions/checkout@v3 - - name: Download dce-cli artifact uses: actions/download-artifact@v4 with: @@ -341,63 +390,47 @@ jobs: - name: Copy Artifacts to Working Dir run: | set -ex - - # GitHub Actions wraps artifact files inside a directory - # in the github.workspace dir (which is different than our working dir...) - # Extract these out into our working dir, for easier access cp ${{ github.workspace }}/dce-cli/dce ./ cp ${{ github.workspace }}/dce-yml/dce.yml ./ cp ${{ github.workspace }}/lease_id/lease_id.txt ./ cp ${{ github.workspace }}/namespace/namespace.txt ./ cp ${{ github.workspace }}/backend-tf/backend.tf ./modules/ - chmod +x ./dce - - name: DCE Lease Login - env: - AWS_DEFAULT_REGION: us-east-1 - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: | - set -ex - lease_id=$(cat ./lease_id.txt) - echo "Logging into lease ${lease_id}" - - cat ./dce.yml - - ./dce --config=./dce.yml leases login ${lease_id} - - # Cleanup the PR environment - # Install Terraform - - name: Install Terraform + # Cleanup the PR environment + - name: Terraform for Cleanup uses: hashicorp/setup-terraform@v2 with: terraform_version: ${{ inputs.terraform_version }} - name: Terraform destroy + env: + NAMESPACE: ${{ env.namespace }} + NOTIFY_EMAIL: ${{ secrets.NOTIFY_EMAIL }} + TF_VAR_global_tags: ${{ vars.TF_VAR_GLOBAL_TAGS}} run: | set -ex export TF_VAR_namespace=$(cat ./namespace.txt) - export TF_VAR_budget_notification_from_email=${{ env.NOTIFY_EMAIL }} + export TF_VAR_budget_notification_from_email="${NOTIFY_EMAIL}" + export TF_VAR_global_tags='${{ vars.TF_VAR_GLOBAL_TAGS}}' + export TF_VAR_required_bucket_prefix="${REQUIRED_BUCKET_PREFIX}" cd modules terraform init -input=false terraform destroy -auto-approve - - # End the DCE lease + + # End the DCE lease - name: End DCE Lease env: AWS_DEFAULT_REGION: us-east-1 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY}} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | set -ex lease_id=$(cat ./lease_id.txt) namespace=$(cat ./namespace.txt) - leases=$( - ./dce --config=dce.yml leases list -s Active \ - ) + leases=$(./dce --config=dce.yml leases list -s Active) account_id=$( echo $leases | \ jq -r \ @@ -405,77 +438,94 @@ jobs: '.[] | select( .id==$Id ) | .accountId' ) - echo "Ending lease ${lease_id}" - ./dce --config=./dce.yml leases end \ -p ${namespace} \ -a ${account_id} - + Release: needs: [Deploy] runs-on: ubuntu-latest - if: ${{ github.event.action == 'published'}} + if: ${{ github.event.action == 'published' || github.event.action == 'prereleased' }} steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Download bin artifacts - uses: actions/download-artifact@v4 - with: - name: bin - path: ./bin - - - name: Download deploy_scripts artifacts - uses: actions/download-artifact@v4 - with: - name: deploy_scripts - path: ./deploy_scripts - - - name: Create GitHub Release - uses: actions/create-release@v1 - with: - tag_name: ${{ github.ref_name }} - release_name: Release ${{ github.ref_name }} - body: | - This release includes the following artifacts: - - build_artifacts.zip - - terraform_artifacts.zip - - deploy.sh - - restore_db.sh - draft: false - prerelease: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Release Assets - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/build_artifacts.zip - asset_name: build_artifacts.zip - asset_content_type: application/zip - - - name: Upload Terraform Artifacts - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/terraform_artifacts.zip - asset_name: terraform_artifacts.zip - asset_content_type: application/zip - - - name: Upload Deploy Script - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./deploy_scripts/deploy.sh - asset_name: deploy.sh - asset_content_type: text/x-shellscript - - - name: Upload Restore DB Script - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./deploy_scripts/restore_db.sh - asset_name: restore_db.sh - asset_content_type: text/x-shellscript - \ No newline at end of file + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Download bin artifacts + uses: actions/download-artifact@v4 + with: + name: bin + path: ./bin + + - name: Download deploy_scripts artifacts + uses: actions/download-artifact@v4 + with: + name: deploy_scripts + path: ./deploy_scripts + + - name: Verify Downloaded Artifacts + run: | + [ -f "./bin/build_artifacts.zip" ] && echo "build_artifacts.zip found" || echo "build_artifacts.zip missing" + [ -f "./bin/terraform_artifacts.zip" ] && echo "terraform_artifacts.zip found" || echo "terraform_artifacts.zip missing" + [ -f "./deploy_scripts/deploy.sh" ] && echo "deploy.sh found" || echo "deploy.sh missing" + [ -f "./deploy_scripts/restore_db.sh" ] && echo "restore_db.sh found" || echo "restore_db.sh missing" + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + with: + tag_name: ${{ github.ref_name }} + release_name: Release ${{ github.ref_name }} + body: | + This release includes the following artifacts: + - build_artifacts.zip + - terraform_artifacts.zip + - deploy.sh + - restore_db.sh + draft: true + prerelease: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Build Artifacts + if: ${{ hashFiles('./bin/build_artifacts.zip') != '' }} + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./bin/build_artifacts.zip + asset_name: build_artifacts.zip + asset_content_type: application/zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Terraform Artifacts + if: ${{ hashFiles('./bin/terraform_artifacts.zip') != '' }} + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./bin/terraform_artifacts.zip + asset_name: terraform_artifacts.zip + asset_content_type: application/zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Deploy Script + if: ${{ hashFiles('./deploy_scripts/deploy.sh') != '' }} + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./deploy_scripts/deploy.sh + asset_name: deploy.sh + asset_content_type: text/x-shellscript + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Restore DB Script + if: ${{ hashFiles('./deploy_scripts/restore_db.sh') != '' }} + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./deploy_scripts/restore_db.sh + asset_name: restore_db.sh + asset_content_type: text/x-shellscript + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/cmd/lambda/list_dirty_accounts/list_dirty_accounts.go b/cmd/lambda/list_dirty_accounts/list_dirty_accounts.go new file mode 100644 index 000000000..0d1b091c0 --- /dev/null +++ b/cmd/lambda/list_dirty_accounts/list_dirty_accounts.go @@ -0,0 +1,280 @@ +package main + +import ( + "encoding/csv" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/Optum/dce/pkg/db" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/sts" +) + +// scanAccountsForMissingRequiredBuckets scans all accounts for required S3 buckets +// If accounts don't have such buckets, marks them as NotReady in the database +func scanAccountsForMissingRequiredBuckets(dbSvc db.DBer, filePath, bucket, s3Key string) error { + log.Println("Scanning accounts for missing buckets (excluding Leased accounts)") + + // Get accounts with different statuses and combine them + readyAccounts, err := dbSvc.FindAccountsByStatus(db.Ready) + if err != nil { + log.Printf("Failed to fetch Ready accounts: %s", err) + return err + } + + notReadyAccounts, err := dbSvc.FindAccountsByStatus(db.NotReady) + if err != nil { + log.Printf("Failed to fetch NotReady accounts: %s", err) + return err + } + + // Combine accounts (we check only Ready and NotReady accounts) + var accounts []*db.Account + accounts = append(accounts, readyAccounts...) + accounts = append(accounts, notReadyAccounts...) + + log.Printf("Found %d total accounts to check", len(accounts)) + + awsRegion := os.Getenv("AWS_CURRENT_REGION") + if awsRegion == "" { + awsRegion = "us-east-1" + } + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(awsRegion), + })) + + // Count of accounts marked as NotReady + markedCount := 0 + + // For each account, check for required buckets + for _, account := range accounts { + // Use the account credentials to check for required buckets + hasBucket, err := checkForBuckets(sess, account.ID, dbSvc) + if err != nil { + log.Printf("Error checking required buckets for account %s: %s", account.ID, err) + continue + } + + if !hasBucket { + log.Printf("Account %s is missing required buckets - marking as NotReady", account.ID) + + if account.Metadata == nil { + account.Metadata = make(map[string]interface{}) + } + + account.Metadata["BucketExists"] = false + account.Metadata["BucketNotFound"] = true + account.Metadata["Reason"] = "Required bucket doesn't exist" + + if account.AccountStatus != db.NotReady { + account.AccountStatus = db.NotReady + markedCount++ + } + + err := dbSvc.PutAccount(*account) + if err != nil { + log.Printf("Failed to update account %s status: %s", account.ID, err) + } + } else if account.AccountStatus == db.NotReady { + if requiredBucket, ok := account.Metadata["BucketNotFound"].(bool); ok && requiredBucket { + log.Printf("Account %s has required buckets but was marked as NotReady - updating metadata", account.ID) + account.Metadata["BucketExists"] = true + account.Metadata["BucketNotFound"] = false + + err := dbSvc.PutAccount(*account) + if err != nil { + log.Printf("Failed to update account %s metadata: %s", account.ID, err) + } + } + } + } + + log.Printf("Marked or updated %d accounts due to missing required buckets", markedCount) + return nil +} +// checkForBuckets checks if an account has S3 buckets starting with the required prefix +func checkForBuckets(sess *session.Session, accountID string, dbSvc db.DBer) (bool, error) { + // Retrieve the bucket prefix from the environment variable + bucketPrefix := os.Getenv("REQUIRED_BUCKET_PREFIX") + if bucketPrefix == "" { + return false, fmt.Errorf("REQUIRED_BUCKET_PREFIX environment variable is not set") + } + + // Get the account record from database to retrieve AdminRoleArn + account, err := dbSvc.GetAccount(accountID) + if err != nil { + return false, fmt.Errorf("failed to get account %s from database: %w", accountID, err) + } + + // Use AdminRoleArn from account record + adminRoleArn := account.AdminRoleArn + if adminRoleArn == "" { + return false, fmt.Errorf("AdminRoleArn not found for account %s", accountID) + } + + // Create STS client for assuming roles + stsSvc := sts.New(sess) + sessionName := fmt.Sprintf("Bucket-Check-%s", time.Now().Format("20060102-150405")) + + // Assume the role using the AdminRoleArn from database + log.Printf("Assuming role %s for bucket check", adminRoleArn) + assumeRoleInput := &sts.AssumeRoleInput{ + RoleArn: aws.String(adminRoleArn), + RoleSessionName: aws.String(sessionName), + DurationSeconds: aws.Int64(900), + } + + log.Printf("Attempting to assume role %s in account %s", adminRoleArn, accountID) + assumeRoleOutput, err := stsSvc.AssumeRole(assumeRoleInput) + if err != nil { + return false, fmt.Errorf("failed to assume role %s in account %s: %w", adminRoleArn, accountID, err) + } + + crossAccountCreds := credentials.NewStaticCredentials( + *assumeRoleOutput.Credentials.AccessKeyId, + *assumeRoleOutput.Credentials.SecretAccessKey, + *assumeRoleOutput.Credentials.SessionToken, + ) + + crossAccountConfig := aws.NewConfig(). + WithCredentials(crossAccountCreds). + WithRegion(aws.StringValue(sess.Config.Region)) + + crossAccountSess := session.Must(session.NewSession(crossAccountConfig)) + + s3Svc := s3.New(crossAccountSess) + + result, err := s3Svc.ListBuckets(&s3.ListBucketsInput{}) + if err != nil { + return false, fmt.Errorf("failed to list buckets for account %s using role %s: %w", accountID, adminRoleArn, err) + } + + // Check if any bucket name starts with the required prefix + for _, bucket := range result.Buckets { + if bucket.Name != nil && len(*bucket.Name) >= len(bucketPrefix) && (*bucket.Name)[:len(bucketPrefix)] == bucketPrefix { + log.Printf("Found required bucket %s in account %s using role %s", *bucket.Name, accountID, adminRoleArn) + return true, nil + } + } + + log.Printf("No required buckets found in account %s using role %s", accountID, adminRoleArn) + return false, nil +} +// listNotReadyAccountsToCSV retrieves all accounts with the status "NotReady" from the ACCOUNT_TABLE, +// saves them to a CSV file, and uploads the file to the specified S3 bucket. +func listNotReadyAccountsToCSV(dbSvc db.DBer, filePath, bucket, s3Key string) error { + log.Println("Fetching all accounts with status NotReady from ACCOUNT_TABLE") + + // Query the ACCOUNT_TABLE for accounts with status "NotReady" + accounts, err := dbSvc.FindAccountsByStatus(db.NotReady) + if err != nil { + log.Printf("Failed to fetch accounts: %s", err) + return err + } + + log.Printf("Found %d accounts with status NotReady", len(accounts)) + + // Create or open the CSV file + file, err := os.Create(filePath) + if err != nil { + log.Printf("Failed to create CSV file: %s", err) + return err + } + defer file.Close() + + // Create a CSV writer + writer := csv.NewWriter(file) + defer writer.Flush() + + // Write the header row with additional field for required Not Found + err = writer.Write([]string{"AccountID", "Status", "LastUpdated", "Reason", "required_Buckets_Not_Found"}) + if err != nil { + log.Printf("Failed to write header to CSV file: %s", err) + return err + } + + // Write account data to the CSV file + for _, account := range accounts { + // Default reason if not specified + reason := "Account marked as NotReady" + + // Default required not found value + BucketNotFound := "false" + + if account.Metadata != nil { + if r, ok := account.Metadata["Reason"].(string); ok && r != "" { + reason = r + } + + // Check if required not found flag is set + if requiredBucket, ok := account.Metadata["BucketNotFound"].(bool); ok && requiredBucket { + BucketNotFound = "true" + } + } + + err := writer.Write([]string{ + account.ID, + string(account.AccountStatus), + time.Unix(account.LastModifiedOn, 0).Format(time.RFC3339), + reason, + BucketNotFound, + }) + if err != nil { + log.Printf("Failed to write account data to CSV file: %s", err) + return err + } + } + + writer.Flush() + + if err := writer.Error(); err != nil { + log.Printf("CSV writer error: %s", err) + return err + } + + file.Close() + + log.Printf("Successfully saved NotReady accounts to %s", filePath) + + // Upload the file to S3 + awsRegion := os.Getenv("AWS_CURRENT_REGION") + if awsRegion == "" { + awsRegion = "us-east-1" + } + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(awsRegion), + })) + s3Svc := s3.New(sess) + fileForUpload, err := os.Open(filePath) + if err != nil { + log.Printf("Failed to open CSV file for upload: %s", err) + return err + } + defer fileForUpload.Close() + + if bucket == "" { + log.Printf("Error: S3 bucket name is empty. Set ARTIFACT_BUCKET_NAME environment variable.") + return fmt.Errorf("S3 bucket name cannot be empty") + } + + _, err = s3Svc.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(s3Key), + Body: fileForUpload, + }) + if err != nil { + log.Printf("Failed to upload file to S3: %s", err) + return err + } + + log.Printf("Successfully uploaded %s to s3://%s/%s", filepath.Base(filePath), bucket, s3Key) + return nil +} \ No newline at end of file diff --git a/cmd/lambda/list_dirty_accounts/list_dirty_accounts_test.go b/cmd/lambda/list_dirty_accounts/list_dirty_accounts_test.go new file mode 100644 index 000000000..dd8306c4b --- /dev/null +++ b/cmd/lambda/list_dirty_accounts/list_dirty_accounts_test.go @@ -0,0 +1,345 @@ +package main + +import ( + "errors" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/Optum/dce/pkg/db" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// mockDB implements db.DBer for testing +type mockDB struct { + accounts []db.Account + err error +} + +func (m *mockDB) ScanAccounts(filter map[string]interface{}) ([]db.Account, error) { + return m.accounts, m.err +} + +func (m *mockDB) FindAccountsByStatus(status db.AccountStatus) ([]*db.Account, error) { + if m.err != nil { + return nil, m.err + } + + var filtered []*db.Account + for i := range m.accounts { + if m.accounts[i].AccountStatus == status { + accountCopy := m.accounts[i] + filtered = append(filtered, &accountCopy) + } + } + + return filtered, nil +} + +// UpdateAccount is a new method to support updating accounts when marking as NotReady +func (m *mockDB) UpdateAccount(account *db.Account) error { + if m.err != nil { + return m.err + } + + for i := range m.accounts { + if m.accounts[i].ID == account.ID { + m.accounts[i] = *account + return nil + } + } + + return errors.New("account not found") +} + +func (m *mockDB) FindLeasesByAccount(accountID string) ([]*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + return []*db.Lease{}, m.err +} + +func (m *mockDB) FindLeasesByPrincipal(principalID string) ([]*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + return []*db.Lease{}, m.err +} + +func (m *mockDB) FindLeasesByStatus(status db.LeaseStatus) ([]*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + return []*db.Lease{}, m.err +} + +func (m *mockDB) GetAccount(accountID string) (*db.Account, error) { + if m.err != nil { + return nil, m.err + } + + for i := range m.accounts { + if m.accounts[i].ID == accountID { + accountCopy := m.accounts[i] + return &accountCopy, nil + } + } + + return nil, errors.New("account not found") +} + +func (m *mockDB) GetLease(leaseID string, accountID string) (*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + return nil, m.err +} + +// GetLeases is a stub implementation to satisfy db.DBer interface +func (m *mockDB) GetLeases(input db.GetLeasesInput) (db.GetLeasesOutput, error) { + // This is a stub implementation since it's not used in these tests + return db.GetLeasesOutput{}, m.err +} + +// GetLeaseByID is a stub implementation to satisfy db.DBer interface +func (m *mockDB) GetLeaseByID(leaseID string) (*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + return nil, m.err +} + +// GetReadyAccount is a stub implementation to satisfy db.DBer interface +func (m *mockDB) GetReadyAccount() (*db.Account, error) { + if m.err != nil { + return nil, m.err + } + for i := range m.accounts { + if m.accounts[i].AccountStatus == db.Ready { + accountCopy := m.accounts[i] + return &accountCopy, nil + } + } + return nil, errors.New("ready account not found") +} + +// OrphanAccount is a stub implementation to satisfy db.DBer interface +func (m *mockDB) OrphanAccount(accountID string) (*db.Account, error) { + if m.err != nil { + return nil, m.err + } + + // Find and return the account being orphaned + for i := range m.accounts { + if m.accounts[i].ID == accountID { + accountCopy := m.accounts[i] + return &accountCopy, nil + } + } + + // Return nil account if not found (or you could return an error) + return nil, errors.New("account not found") +} + +// PutAccount is a stub implementation to satisfy db.DBer interface +func (m *mockDB) PutAccount(account db.Account) error { + if m.err != nil { + return m.err + } + + // Check if account already exists and update it + for i := range m.accounts { + if m.accounts[i].ID == account.ID { + m.accounts[i] = account + return nil + } + } + + // If account doesn't exist, add it + m.accounts = append(m.accounts, account) + return nil +} + +// PutLease is a stub implementation to satisfy db.DBer interface +func (m *mockDB) PutLease(lease db.Lease) (*db.Lease, error) { + // This is a stub implementation since it's not used in these tests + if m.err != nil { + return nil, m.err + } + return &lease, nil +} + +// TransitionAccountStatus is a stub implementation to satisfy db.DBer interface +func (m *mockDB) TransitionAccountStatus(accountID string, fromStatus db.AccountStatus, toStatus db.AccountStatus) (*db.Account, error) { + if m.err != nil { + return nil, m.err + } + + for i := range m.accounts { + if m.accounts[i].ID == accountID && m.accounts[i].AccountStatus == fromStatus { + m.accounts[i].AccountStatus = toStatus + accountCopy := m.accounts[i] + return &accountCopy, nil + } + } + + return nil, errors.New("account not found or status transition not valid") +} + +// TransitionLeaseStatus is a stub implementation to satisfy db.DBer interface +func (m *mockDB) TransitionLeaseStatus(leaseID string, accountID string, fromStatus db.LeaseStatus, toStatus db.LeaseStatus, reason db.LeaseStatusReason) (*db.Lease, error) { + if m.err != nil { + return nil, m.err + } + + // This is a stub implementation since it's not used in these tests + return nil, errors.New("lease not found or status transition not valid") +} + +// UpdateAccountPrincipalPolicyHash is a stub implementation to satisfy db.DBer interface +func (m *mockDB) UpdateAccountPrincipalPolicyHash(accountID string, principalID string, principalPolicyHash string) (*db.Account, error) { + if m.err != nil { + return nil, m.err + } + + // This is a stub implementation since it's not used in these tests + // Find and return the account if it exists + for i := range m.accounts { + if m.accounts[i].ID == accountID { + accountCopy := m.accounts[i] + return &accountCopy, nil + } + } + + return nil, errors.New("account not found") +} + +// UpsertLease is a stub implementation to satisfy db.DBer interface +func (m *mockDB) UpsertLease(lease db.Lease) (*db.Lease, error) { + if m.err != nil { + return nil, m.err + } + + // This is a stub implementation since it's not used in these tests + return &lease, nil +} + +// Mock for the checkForBuckets function +type mockBucketChecker struct { + mock.Mock +} + +func (m *mockBucketChecker) checkForBuckets(sess *session.Session, accountID string) (bool, error) { + args := m.Called(sess, accountID) + return args.Bool(0), args.Error(1) +} + +// Override the real function with our mock for testing +var mockBucketChecker = &mockBucketChecker{} + +func TestScanAccountsForMissingBuckets(t *testing.T) { + // Setup mock accounts with different statuses + mockAccounts := []db.Account{ + {ID: "123", AccountStatus: db.Ready, LastModifiedOn: time.Now().Unix()}, + {ID: "456", AccountStatus: db.Leased, LastModifiedOn: time.Now().Unix()}, + {ID: "789", AccountStatus: db.NotReady, LastModifiedOn: time.Now().Unix()}, + } + + dbSvc := &mockDB{accounts: mockAccounts} + + // Mock the bucket checker to return false (no buckets) for account 123 + // and true (has buckets) for account 456 + mockBucketChecker.On("checkForBuckets", mock.Anything, "123").Return(false, nil) + mockBucketChecker.On("checkForBuckets", mock.Anything, "456").Return(true, nil) + + // Call the function with a dummy file path + err := scanAccountsForMissingRequiredBuckets(dbSvc, "test.csv", "test-bucket", "test-key") + + // Verify no error + assert.NoError(t, err) + + // Account 123 should now be marked as NotReady + for _, account := range dbSvc.accounts { + if account.ID == "123" { + assert.Equal(t, db.NotReady, account.AccountStatus) + assert.NotNil(t, account.Metadata) + assert.Equal(t, true, account.Metadata["BucketNotFound"]) + } + if account.ID == "456" { + // Account 456 should still be Leased + assert.Equal(t, db.Leased, account.AccountStatus) + } + } +} + +func TestListNotReadyAccountsToCSV_Success(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "not_ready_accounts_*.csv") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + // Create mock accounts with the new BucketNotFound field in metadata + mockAccounts := []db.Account{ + { + ID: "123", + AccountStatus: db.NotReady, + LastModifiedOn: time.Now().Unix(), + Metadata: map[string]interface{}{ + "Reason": "Required bucket doesn't exist", + "BucketNotFound": true, + }, + }, + { + ID: "456", + AccountStatus: db.NotReady, + LastModifiedOn: time.Now().Unix(), + Metadata: map[string]interface{}{ + "Reason": "Other reason", + "BucketNotFound": false, + }, + }, + } + dbSvc := &mockDB{accounts: mockAccounts} + + // Use a dummy bucket and key since we are not actually uploading in this test + err = listNotReadyAccountsToCSV(dbSvc, tmpfile.Name(), "dummy-bucket", "dummy-key") + assert.NoError(t, err) + + // Check that the CSV file was written with the expected content + content, err := ioutil.ReadFile(tmpfile.Name()) + assert.NoError(t, err) + csvContent := string(content) + + // Check for header with the new Bucket_Not_Found field + assert.Contains(t, csvContent, "AccountID,Status,LastUpdated,Reason,Bucket_Not_Found") + + // Check that account 123 has true for Bucket_Not_Found + assert.Contains(t, csvContent, "123,NotReady,") + assert.Contains(t, csvContent, "Required bucket doesn't exist,true") + + // Check that account 456 has false for Bucket_Not_Found + assert.Contains(t, csvContent, "456,NotReady,") + assert.Contains(t, csvContent, "Other reason,false") +} + +func TestListNotReadyAccountsToCSV_DBError(t *testing.T) { + dbSvc := &mockDB{err: errors.New("db error")} + err := listNotReadyAccountsToCSV(dbSvc, "dummy.csv", "dummy-bucket", "dummy-key") + assert.Error(t, err) +} + +func TestListNotReadyAccountsToCSV_CSVWriteError(t *testing.T) { + // Simulate a file that cannot be created + dbSvc := &mockDB{accounts: []db.Account{{ID: "123", AccountStatus: db.NotReady, LastModifiedOn: time.Now().Unix()}}} + // Use an invalid file path to force an error + err := listNotReadyAccountsToCSV(dbSvc, "/invalid/path/not_ready_accounts.csv", "dummy-bucket", "dummy-key") + assert.Error(t, err) +} + +func TestCheckForBuckets(t *testing.T) { + // This would be an integration test requiring AWS credentials + // In a real test suite, you would mock the AWS SDK calls + // Here we're just testing the mocking setup we created + + sess := session.Must(session.NewSession()) + + mockBucketChecker.On("checkForBuckets", sess, "test-account").Return(true, nil) + + hasBucket, err := mockBucketChecker.checkForBuckets(sess, "test-account") + + assert.NoError(t, err) + assert.True(t, hasBucket) + mockBucketChecker.AssertExpectations(t) +} \ No newline at end of file diff --git a/cmd/lambda/list_dirty_accounts/main.go b/cmd/lambda/list_dirty_accounts/main.go new file mode 100644 index 000000000..c65217e08 --- /dev/null +++ b/cmd/lambda/list_dirty_accounts/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/Optum/dce/pkg/db" +) + +func initializeDBService() db.DBer { + // Initialize DB connection from environment variables + dao, err := db.NewFromEnv() + if err != nil { + log.Fatalf("Failed to initialize database: %s", err) + } + return dao +} + +func main() { + dbSvc := initializeDBService() + + // Verify required environment variables + requiredBucketPrefix := os.Getenv("REQUIRED_BUCKET_PREFIX") + if requiredBucketPrefix == "" { + log.Fatalf("REQUIRED_BUCKET_PREFIX environment variable must be set") + } + log.Printf("Using REQUIRED_BUCKET_PREFIX: %s", requiredBucketPrefix) + + // Assert that dbSvc implements the db.DBer interface + var _ db.DBer = dbSvc + + // Generate the current date string + currentDate := time.Now().Format("2006-01-02") + + // 1. First call scanAccountsForMissingRequiredBuckets - this now marks accounts as NotReady in DB + // Use dynamic prefix from environment variable instead of hardcoded value + prefixFilePath := fmt.Sprintf("Missing_%s_Buckets_%s.csv", requiredBucketPrefix, currentDate) + bucket := os.Getenv("ARTIFACT_BUCKET_NAME") + if bucket == "" { + log.Fatalf("ARTIFACT_BUCKET_NAME environment variable must be set") + } + prefixS3Key := fmt.Sprintf("MissingBucketAccounts/%s", prefixFilePath) + + log.Printf("Starting scan for accounts missing %s buckets...", requiredBucketPrefix) + err := scanAccountsForMissingRequiredBuckets(dbSvc, prefixFilePath, bucket, prefixS3Key) + if err != nil { + log.Printf("Error scanning for %s buckets: %s", requiredBucketPrefix, err) + } + + // 2. Then call listNotReadyAccountsToCSV - this now includes bucket status field + notReadyFilePath := fmt.Sprintf("not_ready_accounts_%s.csv", currentDate) + notReadyS3Key := fmt.Sprintf("NotReadyAccounts/%s", notReadyFilePath) + + log.Printf("Starting scan for NotReady accounts...") + err = listNotReadyAccountsToCSV(dbSvc, notReadyFilePath, bucket, notReadyS3Key) + if err != nil { + log.Fatalf("Error listing NotReady accounts: %s", err) + } + + log.Println("Both report functions completed successfully") +} \ No newline at end of file diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index 15007b2b6..f24c9699a 100755 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -5,8 +5,10 @@ variable "environment" { type = map(string) default = { TERRAFORM = "true" } } + variable "global_tags" { - type = map(string) + type = map(string) + description = "A map of global tags to apply to all resources" } variable "name" { type = string diff --git a/modules/list_dirty_accounts.tf b/modules/list_dirty_accounts.tf new file mode 100644 index 000000000..63c17c400 --- /dev/null +++ b/modules/list_dirty_accounts.tf @@ -0,0 +1,58 @@ +resource "aws_lambda_function" "list_dirty_accounts" { + function_name = "list_dirty_accounts-${var.namespace}" + handler = "list_dirty_accounts" + runtime = "provided.al2023" + role = module.list_dirty_accounts_lambda.execution_role_arn + filename = "${path.module}/lambda_stub.zip" + + environment { + variables = { + ACCOUNT_DB = aws_dynamodb_table.accounts.id + BUCKET = aws_s3_bucket.artifacts.id + S3_KEY = "dirty_accounts.csv" + NAMESPACE = var.namespace + REQUIRED_BUCKET_PREFIX = var.required_bucket_prefix + } + } + + tags = var.global_tags +} +module "list_dirty_accounts_lambda" { + source = "./lambda" + name = "list-dirty-accounts-${var.namespace}" + namespace = var.namespace + description = "Handles API requests to the /list dirty accounts endpoint" + global_tags = var.global_tags + handler = "list_dirty_accounts" + alarm_topic_arn = aws_sns_topic.alarms_topic.arn + + environment = { + DEBUG = "false" + BUCKET = aws_s3_bucket.artifacts.id + S3_KEY = "dirty_accounts.csv" + NAMESPACE = var.namespace + AWS_CURRENT_REGION = var.aws_region + ACCOUNT_DB = aws_dynamodb_table.accounts.id + } +} + +resource "aws_cloudwatch_event_rule" "list_dirty_accounts_schedule" { + name = "list-dirty-accounts-schedule-${var.namespace}" + description = "Runs list_dirty_accounts Lambda every Sunday at 1 AM CST" + schedule_expression = "cron(0 7 ? * SUN *)" +} + +resource "aws_cloudwatch_event_target" "list_dirty_accounts_lambda_target" { + rule = aws_cloudwatch_event_rule.list_dirty_accounts_schedule.name + target_id = "list-dirty-accounts" + arn = module.list_dirty_accounts_lambda.arn +} + +resource "aws_lambda_permission" "allow_cloudwatch_to_invoke_list_dirty_accounts" { + + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = module.list_dirty_accounts_lambda.name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.list_dirty_accounts_schedule.arn +} diff --git a/modules/variables.tf b/modules/variables.tf index ae06fea71..7707766b3 100755 --- a/modules/variables.tf +++ b/modules/variables.tf @@ -19,17 +19,15 @@ variable "account_pool_metrics_widget_period" { } variable "global_tags" { - description = "The tags to apply to all resources that support tags" type = map(string) - - default = { - Terraform = "True" - AppName = "DCE" - } + description = "A map of global tags to apply to all resources" + default = {} } variable "namespace" { + type = string description = "The namespace for this Terraform run" + default = "dce" } variable "reset_nuke_template_bucket" { @@ -278,3 +276,8 @@ variable "usage_table_wcu" { default = 5 description = "DynamoDB Usage table provisioned Write Capacity Units (WCUs). See https://aws.amazon.com/dynamodb/pricing/provisioned/" } +variable "required_bucket_prefix" { + description = "The prefix for required S3 buckets to check" + type = string + default = "" +} \ No newline at end of file diff --git a/tests/acceptance/api_test.go b/tests/acceptance/api_test.go index e3335de65..5208da2ed 100755 --- a/tests/acceptance/api_test.go +++ b/tests/acceptance/api_test.go @@ -97,6 +97,7 @@ func TestApi(t *testing.T) { ) } + type leaseRequest struct { PrincipalID string `json:"principalId"` AccountID string `json:"accountId"` diff --git a/tests/acceptance/artifacts_bucket_test.go b/tests/acceptance/artifacts_bucket_test.go index 93ea027be..7d96910a8 100755 --- a/tests/acceptance/artifacts_bucket_test.go +++ b/tests/acceptance/artifacts_bucket_test.go @@ -50,5 +50,4 @@ func TestArtifactsBucket(t *testing.T) { require.Nil(t, err) require.Equal(t, *versioningOutput.Status, "Enabled") }) - } diff --git a/tests/acceptance/credentials_web_page_test.go b/tests/acceptance/credentials_web_page_test.go index f0c27ae33..e1f6903ff 100644 --- a/tests/acceptance/credentials_web_page_test.go +++ b/tests/acceptance/credentials_web_page_test.go @@ -18,9 +18,10 @@ func TestCredentialsWebPageLoads(t *testing.T) { tfOpts := &terraform.Options{ TerraformDir: "../../modules", } + tfOut := terraform.OutputAll(t, tfOpts) apiURL := tfOut["api_url"].(string) - + var chainCredentials = credentials.NewChainCredentials([]credentials.Provider{ &credentials.EnvProvider{}, &credentials.SharedCredentialsProvider{Filename: "", Profile: ""},